diff --git a/andmore-core/features/feature/feature.xml b/andmore-core/features/feature/feature.xml index f8ca1f0c..1e756cdb 100644 --- a/andmore-core/features/feature/feature.xml +++ b/andmore-core/features/feature/feature.xml @@ -28,7 +28,6 @@ version="0.0.0"/> - diff --git a/andmore-core/plugins/android.codeutils/.classpath b/andmore-core/plugins/android.codeutils/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/android.codeutils/.classpath +++ b/andmore-core/plugins/android.codeutils/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF b/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF index 842c005b..677a0a19 100644 --- a/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF @@ -1,34 +1,34 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.codeutils;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.codeutils.CodeUtilsActivator -Bundle-Vendor: %providerName -Bundle-ActivationPolicy: lazy -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.codeutils.codegeneration, - org.eclipse.andmore.android.codeutils.db.actions, - org.eclipse.andmore.android.codeutils.db.utils, - org.eclipse.andmore.android.codeutils.wizards, - org.eclipse.andmore.android.db.deployment, - org.eclipse.andmore.android.db.wizards.model, - org.eclipse.andmore.wizards.buildingblocks -Bundle-ClassPath: . -Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.datatools.connectivity, - org.eclipse.datatools.modelbase.sql, - org.eclipse.jdt.ui, - org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.jface.text, - org.eclipse.datatools.connectivity.sqm.core, - org.eclipse.jdt.core, - org.apache.xerces, - org.eclipse.datatools.sqltools.data.ui, - org.eclipse.sequoyah.localization.tools -Import-Package: org.eclipse.ui.texteditor +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.codeutils;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.codeutils.CodeUtilsActivator +Bundle-Vendor: %providerName +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.codeutils.codegeneration, + org.eclipse.andmore.android.codeutils.db.actions, + org.eclipse.andmore.android.codeutils.db.utils, + org.eclipse.andmore.android.codeutils.wizards, + org.eclipse.andmore.android.db.deployment, + org.eclipse.andmore.android.db.wizards.model, + org.eclipse.andmore.wizards.buildingblocks +Bundle-ClassPath: . +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.datatools.connectivity, + org.eclipse.datatools.modelbase.sql, + org.eclipse.jdt.ui, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.jface.text, + org.eclipse.datatools.connectivity.sqm.core, + org.eclipse.jdt.core, + org.apache.xerces, + org.eclipse.datatools.sqltools.data.ui, + org.eclipse.sequoyah.localization.tools +Import-Package: org.eclipse.ui.texteditor diff --git a/andmore-core/plugins/android.win32.x86_64/.classpath b/andmore-core/plugins/android.win32.x86_64/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/android.win32.x86_64/.classpath +++ b/andmore-core/plugins/android.win32.x86_64/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF b/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF index 7acf665a..b3ee5082 100644 --- a/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF @@ -1,10 +1,10 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.android.win32.x86_64;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %Bundle-Vendor -Fragment-Host: org.eclipse.andmore.android -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: fragment -Eclipse-PlatformFilter: (& (osgi.os=win32) (osgi.arch=x86_64)) +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.android.win32.x86_64;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Fragment-Host: org.eclipse.andmore.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: fragment +Eclipse-PlatformFilter: (& (osgi.os=win32) (osgi.arch=x86_64)) diff --git a/andmore-core/plugins/android/.classpath b/andmore-core/plugins/android/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/android/.classpath +++ b/andmore-core/plugins/android/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android/META-INF/MANIFEST.MF b/andmore-core/plugins/android/META-INF/MANIFEST.MF index 7f7ada1e..51e0e04d 100644 --- a/andmore-core/plugins/android/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android/META-INF/MANIFEST.MF @@ -1,46 +1,48 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.AndroidPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android.logger.collector, - org.eclipse.core.runtime, - org.eclipse.core.filesystem, - org.eclipse.andmore, - org.eclipse.andmore.ddms, - org.eclipse.jdt.core, - org.eclipse.jface.text, - org.apache.xerces, - org.eclipse.ui.console, - org.eclipse.sequoyah.localization.tools, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.debug.ui, - org.eclipse.core.expressions, - org.eclipse.andmore.base, - org.eclipse.jdt.ui, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: - org.eclipse.andmore.android, - org.eclipse.andmore.android.devices, - org.eclipse.andmore.android.i18n, - org.eclipse.andmore.android.model, - org.eclipse.andmore.android.multidex, - org.eclipse.andmore.android.nativeos, - org.eclipse.andmore.android.sdkmanager, - org.eclipse.andmore.android.utilities, - org.eclipse.andmore.android.wizards.elements, - org.eclipse.andmore.android.wizards.installapp, - org.eclipse.andmore.android.wizards.monkey -Bundle-ActivationPolicy: lazy -Import-Package: org.eclipse.equinox.security.storage, - org.eclipse.ui.console -Bundle-ClassPath: . +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.AndroidPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android.logger.collector, + org.eclipse.core.runtime, + org.eclipse.core.filesystem, + org.eclipse.andmore, + org.eclipse.andmore.ddms, + org.eclipse.jdt.core, + org.eclipse.jface.text, + org.apache.xerces, + org.eclipse.ui.console, + org.eclipse.sequoyah.localization.tools, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.debug.ui, + org.eclipse.core.expressions, + org.eclipse.jdt.ui, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8", + org.eclipse.andmore.swt;bundle-version="0.5.2", + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2", + org.eclipse.andmore.sdkuilib;bundle-version="0.5.2" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: + org.eclipse.andmore.android, + org.eclipse.andmore.android.devices, + org.eclipse.andmore.android.i18n, + org.eclipse.andmore.android.model, + org.eclipse.andmore.android.multidex, + org.eclipse.andmore.android.nativeos, + org.eclipse.andmore.android.sdkmanager, + org.eclipse.andmore.android.utilities, + org.eclipse.andmore.android.wizards.elements, + org.eclipse.andmore.android.wizards.installapp, + org.eclipse.andmore.android.wizards.monkey +Bundle-ActivationPolicy: lazy +Import-Package: org.eclipse.equinox.security.storage, + org.eclipse.ui.console +Bundle-ClassPath: . diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java index 393fc889..0fe79f7c 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java @@ -1,950 +1,951 @@ -/* - * 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 org.eclipse.andmore.android; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.eclipse.andmore.AndmoreAndroidPlugin; -import org.eclipse.andmore.android.common.IAndroidConstants; -import org.eclipse.andmore.android.common.exception.AndroidException; -import org.eclipse.andmore.android.common.log.AndmoreLogger; -import org.eclipse.andmore.android.common.utilities.FileUtil; -import org.eclipse.andmore.android.i18n.AndroidNLS; -import org.eclipse.andmore.android.manifest.AndroidProjectManifestFile; -import org.eclipse.andmore.android.model.manifest.AndroidManifestFile; -import org.eclipse.andmore.android.model.manifest.dom.AndroidManifestNode; -import org.eclipse.andmore.android.model.manifest.dom.UsesSDKNode; -import org.eclipse.andmore.internal.sdk.AndroidTargetData; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.osgi.util.NLS; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.PlatformUI; - -import com.android.SdkConstants; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdkuilib.internal.widgets.MessageBoxLog; -import com.android.utils.ILogger; - -/** - * DESCRIPTION: This class provides utility methods related to the Android SDK.
- * USAGE: See public methods - */ - -public class SdkUtils { - public static final int API_LEVEL_FOR_PLATFORM_VERSION_3_0_0 = 11; - - public static final String VM_CONFIG_FILENAME = "config.ini"; //$NON-NLS-1$ - - public static final String USERIMAGE_FILENAME = "userdata-qemu.img"; //$NON-NLS-1$ - - public static final String[] STATEDATA_FILENAMES = { "cache.img", "userdata.img", "emulator-user.ini" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - }; - - public static final String EMU_CONFIG_SKIN_NAME_PROPERTY = "skin.name"; //$NON-NLS-1$ - - /** - * Gets the current SDK object - */ - public static Sdk getCurrentSdk() { - return Sdk.getCurrent(); - } - - /** - * Gets the directory where the configured SDK is located - */ - public static String getSdkPath() { - String sdkDir = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - sdkDir = sdk.getSdkOsLocation(); - } - return sdkDir; - } - - /** - * Gets the path to the "tools" folder of the SDK - * - * @return - */ - public static String getSdkToolsPath() { - return AndmoreAndroidPlugin.getOsSdkToolsFolder(); - } - - public static IAndroidTarget getTargetByAPILevel(Integer apiLevel) { - IAndroidTarget returnTarget = null; - - for (IAndroidTarget target : getAllTargets()) { - if (target.getVersion().getApiLevel() == apiLevel) { - returnTarget = target; - } - } - return returnTarget; - } - - /** - * Get the AAPT application path from an android target if null is passed, - * try to get some aapt - * - * @param target - * @return - */ - public static String getTargetAAPTPath(IAndroidTarget target) { - IAndroidTarget realTarget = null; - if (target == null) { - AndmoreLogger.warn(SdkUtils.class, "Trying to find a suitable aapt application to use"); //$NON-NLS-1$ - IAndroidTarget[] allTargets = Sdk.getCurrent().getTargets(); - if (allTargets.length > 0) { - realTarget = allTargets[0]; - } - } else { - realTarget = target; - } - - while ((realTarget != null) && !realTarget.isPlatform()) { - realTarget = realTarget.getParent(); - } - - if (realTarget == null) { - AndmoreLogger.warn(SdkUtils.class, "No aapt executable found: do you have an Android platform installed?"); //$NON-NLS-1$ - } - - return realTarget != null ? realTarget.getPath(IAndroidTarget.ANDROID_JAR) : null; - } - - /** - * Gets the path to the "adb" executable of the SDK - * - * @return - */ - public static String getAdbPath() { - return AndmoreAndroidPlugin.getOsAbsoluteAdb(); - } - - /** - * Reloads the recognized AVD list - */ - public static void reloadAvds() { - - try { - getVmManager().reloadAvds(NullSdkLogger.getLogger()); - } catch (Exception e) { - AndmoreLogger.error(SdkUtils.class, "Error while reloading AVDs"); //$NON-NLS-1$ - } - - } - - protected static class NullSdkLogger implements ILogger { - - private static NullSdkLogger logger; - - private NullSdkLogger() { - - } - - public static ILogger getLogger() { - if (logger == null) { - logger = new NullSdkLogger(); - } - return logger; - } - - @Override - public void error(Throwable arg0, String arg1, Object... arg2) { - - } - - @Override - public void info(String arg0, Object... arg1) { - - } - - @Override - public void verbose(String arg0, Object... arg1) { - - } - - @Override - public void warning(String arg0, Object... arg1) { - - } - - } - - /** - * Gets the VmManager object - */ - public static AvdManager getVmManager() { - AvdManager vmManager = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - vmManager = sdk.getAvdManager(); - } - - return vmManager; - } - - /** - * Gets all available Targets - */ - public static IAndroidTarget[] getAllTargets() { - IAndroidTarget[] allTargets = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - allTargets = sdk.getTargets(); - } - return allTargets; - } - - /** - * Gets a Target by name - * - * @param name - * the target name - */ - public static IAndroidTarget getTargetByName(String name) { - IAndroidTarget ret = null; - - if ((name != null) && (name.length() > 0)) { - IAndroidTarget[] allTargets = getAllTargets(); - - for (int i = 0; i < allTargets.length; i++) { - if (name.equals(allTargets[i].getName())) { - ret = allTargets[i]; - break; - } - } - } - - return ret; - } - - /** - * Gets all VMs - */ - public static AvdInfo[] getAllVms() { - AvdInfo[] allVmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - allVmInfo = vmManager.getAllAvds(); - } - return allVmInfo; - } - - /** - * Gets all valid VMs - */ - public static AvdInfo[] getAllValidVms() { - AvdInfo[] validVmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - validVmInfo = vmManager.getValidAvds(); - } - return validVmInfo; - } - - /** - * Gets the name of all VMs that are recognized by the configured SDK. - */ - public static Collection getAllVmNames() { - Collection vmNames = new LinkedList(); - for (AvdInfo vm : getAllVms()) { - vmNames.add(vm.getName()); - } - return vmNames; - } - - /** - * Gets the name of all VMs that are recognized by the configured SDK. - */ - public static Collection getAllValidVmNames() { - Collection vmNames = new LinkedList(); - AvdInfo[] allAvds = getAllValidVms(); - if (allAvds != null) { - for (AvdInfo vm : allAvds) { - vmNames.add(vm.getName()); - } - } - - return vmNames; - } - - /** - * Gets a skin name - * - * @param vmInfo - * the VM to get the skin - */ - public static String getSkin(AvdInfo vmInfo) { - String skin = ""; //$NON-NLS-1$ - File configFile = vmInfo.getConfigFile(); - Properties p = new Properties(); - InputStream configFileStream = null; - try { - configFileStream = new FileInputStream(configFile); - p.load(configFileStream); - skin = p.getProperty(EMU_CONFIG_SKIN_NAME_PROPERTY); - } catch (FileNotFoundException e) { - AndmoreLogger.error(SdkUtils.class, - "Error getting VM skin definition. Could not find file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ - } catch (IOException e) { - AndmoreLogger.error(SdkUtils.class, - "Error getting VM skin definition. Could not access file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ - } finally { - if (configFileStream != null) { - try { - configFileStream.close(); - } catch (IOException e) { - // nothing to do - } - } - } - - return skin; - } - - /** - * Gets a VM by name. - * - * @param vmName - * The VM name - */ - public static AvdInfo getValidVm(String vmName) { - AvdInfo vmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - vmInfo = vmManager.getAvd(vmName, true); - } - return vmInfo; - } - - /** - * Gets a VM by name. - * - * @param vmName - * The VM name - */ - public static AvdInfo getVm(String vmName) { - AvdInfo vmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - vmInfo = vmManager.getAvd(vmName, false); - } - return vmInfo; - } - - /** - * Creates a new VM instance. - * - * @param folder - * Folder where the VM files will be stored - * @param name - * VM Name - * @param target - * VM Target represented by the IAndroidTarget object - * @param skinName - * VM Skin name from the VM Target - * - * @throws CoreException - */ - public static AvdInfo createVm(String folder, String name, IAndroidTarget target, String abiType, String skinName, - String useSnapshot, String sdCard) throws CoreException - - { - AvdInfo vmInfo; - AvdManager vmManager = SdkUtils.getVmManager(); - - // get the abi type - if (abiType == null) { - abiType = SdkConstants.ABI_ARMEABI; - } - - /* - * public VmInfo createVm(String parentFolder, String name, - * IAndroidTarget target, String skinName, String sdcard, Map - * hardwareConfig) - */ - - // TODO: FIX ME commented out for now. - vmInfo = null; - // vmInfo = vmManager.createAvd(new File(folder), name, target, abiType, - // skinName, sdCard, null, Boolean.parseBoolean(useSnapshot), - // true, false, NullSdkLogger.getLogger()); - - if (vmInfo == null) { - String errMsg = NLS.bind(AndroidNLS.EXC_SdkUtils_CannotCreateTheVMInstance, name); - - IStatus status = new Status(IStatus.ERROR, AndmoreAndroidPlugin.PLUGIN_ID, errMsg); - throw new CoreException(status); - } - - return vmInfo; - } - - /** - * Deletes a VM instance. - * - * @param name - * VM Name - */ - public static void deleteVm(String name) { - - AvdManager vmManager = SdkUtils.getVmManager(); - AvdInfo avdToDelete = vmManager != null ? vmManager.getAvd(name, false) : null; - if (avdToDelete != null) { - try { - if ((avdToDelete.getIniFile() != null) && avdToDelete.getIniFile().exists()) { - avdToDelete.getIniFile().delete(); - } - String path = avdToDelete.getDataFolderPath(); - if (path != null) { - File avdDir = new File(path); - if (avdDir.exists()) { - FileUtil.deleteDirRecursively(avdDir); - } - } - vmManager.removeAvd(avdToDelete); - - } catch (Exception e) { - AndmoreLogger.error("Could not delete AVD: " + e.getMessage()); //$NON-NLS-1$ - } - } - - } - - /** - * Get the reference to the File that points to the filesystem location of - * the directory where the user data files of the VM with the given name are - * stored. - * - * @param vmName - * name of the VM whose userdata directory is to be retrieved. - * @return the File object that references the filesystem location of the - * directory where the userdata files of the given VM will be - * stored. Returns a null reference if SDK is not configured or if - * there is no VM with the given name. - */ - public static File getUserdataDir(String vmName) { - AvdInfo vminfo = SdkUtils.getValidVm(vmName); - File userdataDir = null; - - if (vminfo != null) { - String vmpath = vminfo.getDataFolderPath(); - userdataDir = new File(vmpath); - } - - return userdataDir; - } - - /** - * Get the reference to the File that points to the filesystem location - * where the user data file of the VM with the given name is. - * - * @param vmName - * name of the VM whose userdata file is to be retrieved. - * @return the File object that references the filesystem location where the - * userdata of the given VM should be. Returns a null reference if - * SDK is not configured or if there is no VM with the given name. - */ - public static File getUserdataFile(String vmName) { - File userdataDir = getUserdataDir(vmName); - File userdataFile = null; - - if (userdataDir != null) { - userdataFile = new File(userdataDir, USERIMAGE_FILENAME); - } - - return userdataFile; - } - - /** - * Get the reference to the Files that point to the filesystem location - * where the state data files of the VM with the given name are. - * - * @param vmName - * name of the VM whose state data files is to be retrieved. - * @return the File objects that reference the filesystem location where the - * state data files of the given VM should be. Returns a null - * reference if SDK is not configured or if there is no VM with the - * given name. - */ - public static List getStateDataFiles(String vmName) { - File userdataDir = getUserdataDir(vmName); - List stateDataFiles = null; - - if (userdataDir != null) { - stateDataFiles = new ArrayList(); - - for (int i = 0; i < STATEDATA_FILENAMES.length; i++) { - stateDataFiles.add(new File(userdataDir, STATEDATA_FILENAMES[i])); - } - } - - return stateDataFiles; - } - - /** - * Retrieves all sample applications from a target - * - * @param target - * The target - * @return all sample applications from a target - */ - public static Object[] getSamples(IAndroidTarget target) { - List samples = new ArrayList(); - File samplesFolder = new File(target.getPath(IAndroidTarget.SAMPLES)); - samples = findSamples(samplesFolder, target); - return samples.toArray(); - } - - /** - * Find the samples inside an specific directory (recursively) - * - * @param folder - * the folder that can contain samples - * @param target - * the target of the samples in the folder - * @return a list of samples - */ - private static List findSamples(File folder, IAndroidTarget target) { - - List samples = new ArrayList(); - - if (folder.exists() && folder.isDirectory()) { - for (File sampleFolder : folder.listFiles()) { - if (sampleFolder.isDirectory()) { - if (Sample.isSample(sampleFolder)) { - samples.add(new Sample(sampleFolder, target)); - } else { - samples.addAll(findSamples(sampleFolder, target)); - } - } - } - } - - return samples; - } - - /** - * Retrieves all targets for a given SDK - * - * @param sdk - * The sdk - * - * @return all targets for the given SDK - */ - public static Object[] getTargets(Sdk sdk) { - Object[] targets = null; - if (sdk != null) { - targets = sdk.getTargets(); - } - return targets; - } - - /** - * Associates a project to a target - * - * @param project - * The project - * @param target - * The target - */ - public static void associate(IProject project, IAndroidTarget target) { - try { - Sdk.getCurrent().initProject(project, target); - } catch (Exception e) { - AndmoreLogger.error(SdkUtils.class, "Error associating project " + project.getName() //$NON-NLS-1$ - + " with target " + target.getName()); //$NON-NLS-1$ - } - } - - /** - * Retrieves the target for a project - * - * @param project - * the project - * - * @return the target for the project - */ - public static IAndroidTarget getTarget(IProject project) { - IAndroidTarget target = null; - if (project != null) { - target = Sdk.getCurrent().getTarget(project); - } - return target; - } - - /** - * Retrieves the target for a project - * - * @param project - * the project - * - * @return the target for the project - */ - public static String getMinSdkVersion(IProject project) { - String minSdkVersion = ""; - try { - AndroidManifestFile androidManifestFile = AndroidProjectManifestFile.getFromProject(project); - UsesSDKNode usesSdkNode = (UsesSDKNode) androidManifestFile.getNode(AndroidManifestNode.NodeType.UsesSdk); - if (usesSdkNode != null) { - minSdkVersion = usesSdkNode.getMinSdkVersion(); - } - - } catch (AndroidException e) { - AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); - } catch (CoreException e) { - AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); - } - return minSdkVersion; - } - - /** - * Retrieves all Activity Actions for a project. - * - * @param project - * The project - * - * @return all Activity Actions for the project. - */ - public static String[] getActivityActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "activity"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Service Actions for a project. - * - * @param project - * The project - * - * @return all Service Actions for the project. - */ - public static String[] getServiceActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "service"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Broadcast Receiver Actions for a project. - * - * @param project - * The project - * - * @return all Broadcast Receiver Actions for the project. - */ - public static String[] getReceiverActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "receiver"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Intent Filter Actions for a project. - * - * @param project - * The project - * - * @return all Intent Filter Actions for the project. - */ - public static String[] getIntentFilterCategories(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("category", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - return attributeValues; - } - - /** - * Get the api version number for a given project - * - * @param project - * : the project - * @return the api version number or 0 if some error occurs - */ - public static int getApiVersionNumberForProject(IProject project) { - int api = 0; - IAndroidTarget target = SdkUtils.getTarget(project); - if (target != null) { - AndroidVersion version = target.getVersion(); - if (version != null) { - api = version.getApiLevel(); - } - } - return api; - } - - public static String getTargetNameForProject(IProject project) { - IAndroidTarget target = getTarget(project); - return target != null ? target.getName() : ""; //$NON-NLS-1$ - } - - public static boolean isPlatformTarget(String avdName) { - IAndroidTarget target = getValidVm(avdName).getTarget(); - return target != null ? target.isPlatform() : false; - } - - public static boolean isProjectTargetAPlatform(IProject project) { - IAndroidTarget target = getTarget(project); - return target != null ? target.isPlatform() : false; - } - - public static boolean isPlatformTarget(IAndroidTarget target) { - return target != null ? target.isPlatform() : false; - } - - /** - * Retrieves the APK configurations of a project - * - * @param project - * the project - * @return the APK configurations - */ - public static Map getAPKConfigurationsForProject(IProject project) { - - Map apkConfigurations = null; - - if ((project != null) && project.isOpen()) { - Sdk.getCurrent(); - // This is not supported on ADT 14 preview so let's comment it for - // now. - // apkConfigurations = - // Sdk.getProjectState(project).getApkSettings().getLocaleFilters(); - apkConfigurations = new HashMap(0); - } - - return apkConfigurations; - - } - - public static String getBaseTarget(String name) { - IAndroidTarget target = getValidVm(name).getTarget(); - while (!target.isPlatform()) { - target = target.getParent(); - } - return target.getName(); - } - - /** - * Check if an SDK is an OPhone Sdk - * - * @return - */ - public static boolean isOphoneSDK() { - boolean result = false; - - // check if the folder contains the oms jar - FilenameFilter omsFilenameFilter = new FilenameFilter() { - @Override - public boolean accept(File arg0, String arg1) { - return arg1.equals(IAndroidConstants.OPHONE_JAR); - } - }; - - Sdk sdk = getCurrentSdk(); - IAndroidTarget[] targets = sdk.getTargets(); - for (IAndroidTarget target : targets) { - File folder = new File(target.getLocation()); - if (folder.list(omsFilenameFilter).length > 0) { - result = true; - break; - } - } - - return result; - } - - /** - * Check if an SDK is an JIL sdk - * - * @return - */ - public static boolean isJILSdk() { - boolean result = false; - - // check if the folder contains the oms jar - FilenameFilter jilFilenameFilter = new FilenameFilter() { - - @Override - public boolean accept(File arg0, String arg1) { - return arg1.equals(IAndroidConstants.JIL_JAR); - } - }; - - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - IAndroidTarget[] targets = sdk.getTargets(); - for (IAndroidTarget target : targets) { - File folder = new File(target.getLocation()); - if (folder.list(jilFilenameFilter).length > 0) { - result = true; - break; - } - } - } - - return result; - } - - public static String getEmulatorWindowName(String avdName, int port) { - String windowName = ""; //$NON-NLS-1$ - if (isJILSdk()) { - windowName = "JIL Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else if (isOphoneSDK()) { - windowName = "OPhone Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else { - windowName = port + ":" + avdName; //$NON-NLS-1$ - } - return windowName; - } - - public static boolean isLibraryProject(IProject project) { - return Sdk.getProjectState(project) != null ? Sdk.getProjectState(project).isLibrary() : false; - } - - /** - * Returns all available permissions - * - * @return String array containing the available permissions - */ - public static String[] getIntentFilterPermissions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("uses-permission", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - return attributeValues; - } - - /** - * Try to repair an AVD. Currently only avds with wrong image path are - * repariable. Display a message with the changes to the config.ini - * - * @param avdInfo - * @return Status ERROR if an IO exception occured. - */ - public static IStatus repairAvd(AvdInfo avdInfo) { - IStatus status = Status.OK_STATUS; - - AvdManager avdManager = Sdk.getCurrent().getAvdManager(); - Display display = PlatformUI.getWorkbench().getDisplay(); - ILogger log = new MessageBoxLog(String.format("Result of updating AVD '%s':", avdInfo.getName()), //$NON-NLS-1$ - display, false); - try { - avdManager.updateAvd(avdInfo, log); - // display the result - if (log instanceof MessageBoxLog) { - ((MessageBoxLog) log).displayResult(true); - } - SdkUtils.reloadAvds(); - - } catch (IOException e) { - status = new Status(IStatus.ERROR, AndroidPlugin.PLUGIN_ID, AndroidNLS.SdkUtils_COULD_NOT_REPAIR_AVD, e); - } - - return status; - } - - public static String getDefaultSkin(String targetName) { - IAndroidTarget target = getTargetByName(targetName); - target.getDefaultSkin().getName(); - return target != null ? target.getDefaultSkin().getName() : "HVGA"; - } - - /** - * Returns the full absolute OS path to a skin specified by name for a given - * target. - * - * @param skinName - * The name of the skin to find. Case-sensitive. - * @param target - * The target where to find the skin. - * @return a {@link File} that may or may not actually exist. - */ - public static File getSkinFolder(String skinName, IAndroidTarget target) { - String path = target.getPath(IAndroidTarget.SKINS); - File skin = new File(path, skinName); - - if (skin.exists() == false && target.isPlatform() == false) { - target = target.getParent(); - - path = target.getPath(IAndroidTarget.SKINS); - skin = new File(path, skinName); - } - - return skin; - } -} +/* + * 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 org.eclipse.andmore.android; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.andmore.AndmoreAndroidPlugin; +import org.eclipse.andmore.android.common.IAndroidConstants; +import org.eclipse.andmore.android.common.exception.AndroidException; +import org.eclipse.andmore.android.common.log.AndmoreLogger; +import org.eclipse.andmore.android.common.utilities.FileUtil; +import org.eclipse.andmore.android.i18n.AndroidNLS; +import org.eclipse.andmore.android.manifest.AndroidProjectManifestFile; +import org.eclipse.andmore.android.model.manifest.AndroidManifestFile; +import org.eclipse.andmore.android.model.manifest.dom.AndroidManifestNode; +import org.eclipse.andmore.android.model.manifest.dom.UsesSDKNode; +import org.eclipse.andmore.internal.sdk.AndroidTargetData; +import org.eclipse.andmore.internal.sdk.Sdk; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; + +import com.android.SdkConstants; +import com.android.repository.io.FileOpUtils; +import com.android.repository.testframework.FakeProgressIndicator; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.sdkuilib.widgets.MessageBoxLog; +import com.android.utils.ILogger; + +/** + * DESCRIPTION: This class provides utility methods related to the Android SDK.
+ * USAGE: See public methods + */ + +public class SdkUtils { + public static final int API_LEVEL_FOR_PLATFORM_VERSION_3_0_0 = 11; + + public static final String VM_CONFIG_FILENAME = "config.ini"; //$NON-NLS-1$ + + public static final String USERIMAGE_FILENAME = "userdata-qemu.img"; //$NON-NLS-1$ + + public static final String[] STATEDATA_FILENAMES = { "cache.img", "userdata.img", "emulator-user.ini" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + }; + + public static final String EMU_CONFIG_SKIN_NAME_PROPERTY = "skin.name"; //$NON-NLS-1$ + + /** + * Gets the current SDK object + */ + public static Sdk getCurrentSdk() { + return Sdk.getCurrent(); + } + + /** + * Gets the directory where the configured SDK is located + */ + public static String getSdkPath() { + String sdkDir = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + sdkDir = sdk.getSdkFileLocation().getAbsolutePath(); + } + return sdkDir; + } + + /** + * Gets the path to the "tools" folder of the SDK + * + * @return + */ + public static String getSdkToolsPath() { + return AndmoreAndroidPlugin.getOsSdkToolsFolder(); + } + + public static IAndroidTarget getTargetByAPILevel(Integer apiLevel) { + IAndroidTarget returnTarget = null; + + for (IAndroidTarget target : getAllTargets()) { + if (target.getVersion().getApiLevel() == apiLevel) { + returnTarget = target; + } + } + return returnTarget; + } + + /** + * Get the AAPT application path from an android target if null is passed, + * try to get some aapt + * + * @param target + * @return + */ + public static String getTargetAAPTPath(IAndroidTarget target) { + IAndroidTarget realTarget = null; + if (target == null) { + AndmoreLogger.warn(SdkUtils.class, "Trying to find a suitable aapt application to use"); //$NON-NLS-1$ + Collection allTargets = Sdk.getCurrent().getTargets(); + if (allTargets.size() > 0) { + realTarget = allTargets.iterator().next(); + } + } else { + realTarget = target; + } + + while ((realTarget != null) && !realTarget.isPlatform()) { + realTarget = realTarget.getParent(); + } + + if (realTarget == null) { + AndmoreLogger.warn(SdkUtils.class, "No aapt executable found: do you have an Android platform installed?"); //$NON-NLS-1$ + } + + return realTarget != null ? realTarget.getPath(IAndroidTarget.ANDROID_JAR) : null; + } + + /** + * Gets the path to the "adb" executable of the SDK + * + * @return + */ + public static String getAdbPath() { + return AndmoreAndroidPlugin.getOsAbsoluteAdb(); + } + + /** + * Reloads the recognized AVD list + */ + public static void reloadAvds() { + + try { + getVmManager().reloadAvds(NullSdkLogger.getLogger()); + } catch (Exception e) { + AndmoreLogger.error(SdkUtils.class, "Error while reloading AVDs"); //$NON-NLS-1$ + } + + } + + protected static class NullSdkLogger implements ILogger { + + private static NullSdkLogger logger; + + private NullSdkLogger() { + + } + + public static ILogger getLogger() { + if (logger == null) { + logger = new NullSdkLogger(); + } + return logger; + } + + @Override + public void error(Throwable arg0, String arg1, Object... arg2) { + + } + + @Override + public void info(String arg0, Object... arg1) { + + } + + @Override + public void verbose(String arg0, Object... arg1) { + + } + + @Override + public void warning(String arg0, Object... arg1) { + + } + + } + + /** + * Gets the VmManager object + */ + public static AvdManager getVmManager() { + AvdManager vmManager = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + vmManager = sdk.getAvdManager(); + } + + return vmManager; + } + + /** + * Gets all available Targets + */ + public static IAndroidTarget[] getAllTargets() { + IAndroidTarget[] allTargets = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + allTargets = sdk.getTargets().toArray(new IAndroidTarget[0]); + } + return allTargets; + } + + /** + * Gets a Target by name + * + * @param name + * the target name + */ + public static IAndroidTarget getTargetByName(String name) { + IAndroidTarget ret = null; + + if ((name != null) && (name.length() > 0)) { + IAndroidTarget[] allTargets = getAllTargets(); + + for (int i = 0; i < allTargets.length; i++) { + if (name.equals(allTargets[i].getName())) { + ret = allTargets[i]; + break; + } + } + } + + return ret; + } + + /** + * Gets all VMs + */ + public static AvdInfo[] getAllVms() { + AvdInfo[] allVmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + allVmInfo = vmManager.getAllAvds(); + } + return allVmInfo; + } + + /** + * Gets all valid VMs + */ + public static AvdInfo[] getAllValidVms() { + AvdInfo[] validVmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + validVmInfo = vmManager.getValidAvds(); + } + return validVmInfo; + } + + /** + * Gets the name of all VMs that are recognized by the configured SDK. + */ + public static Collection getAllVmNames() { + Collection vmNames = new LinkedList(); + for (AvdInfo vm : getAllVms()) { + vmNames.add(vm.getName()); + } + return vmNames; + } + + /** + * Gets the name of all VMs that are recognized by the configured SDK. + */ + public static Collection getAllValidVmNames() { + Collection vmNames = new LinkedList(); + AvdInfo[] allAvds = getAllValidVms(); + if (allAvds != null) { + for (AvdInfo vm : allAvds) { + vmNames.add(vm.getName()); + } + } + + return vmNames; + } + + /** + * Gets a skin name + * + * @param vmInfo + * the VM to get the skin + */ + public static String getSkin(AvdInfo vmInfo) { + String skin = ""; //$NON-NLS-1$ + File configFile = vmInfo.getConfigFile(); + Properties p = new Properties(); + InputStream configFileStream = null; + try { + configFileStream = new FileInputStream(configFile); + p.load(configFileStream); + skin = p.getProperty(EMU_CONFIG_SKIN_NAME_PROPERTY); + } catch (FileNotFoundException e) { + AndmoreLogger.error(SdkUtils.class, + "Error getting VM skin definition. Could not find file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ + } catch (IOException e) { + AndmoreLogger.error(SdkUtils.class, + "Error getting VM skin definition. Could not access file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ + } finally { + if (configFileStream != null) { + try { + configFileStream.close(); + } catch (IOException e) { + // nothing to do + } + } + } + + return skin; + } + + /** + * Gets a VM by name. + * + * @param vmName + * The VM name + */ + public static AvdInfo getValidVm(String vmName) { + AvdInfo vmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + vmInfo = vmManager.getAvd(vmName, true); + } + return vmInfo; + } + + /** + * Gets a VM by name. + * + * @param vmName + * The VM name + */ + public static AvdInfo getVm(String vmName) { + AvdInfo vmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + vmInfo = vmManager.getAvd(vmName, false); + } + return vmInfo; + } + + /** + * Creates a new VM instance. + * + * @param folder + * Folder where the VM files will be stored + * @param name + * VM Name + * @param target + * VM Target represented by the IAndroidTarget object + * @param skinName + * VM Skin name from the VM Target + * + * @throws CoreException + */ + public static AvdInfo createVm(String folder, String name, IAndroidTarget target, String abiType, String skinName, + String useSnapshot, String sdCard) throws CoreException + + { + AvdInfo vmInfo; + AvdManager vmManager = SdkUtils.getVmManager(); + + // get the abi type + if (abiType == null) { + abiType = SdkConstants.ABI_ARMEABI; + } + + /* + * public VmInfo createVm(String parentFolder, String name, + * IAndroidTarget target, String skinName, String sdcard, Map + * hardwareConfig) + */ + + // TODO: FIX ME commented out for now. + vmInfo = null; + // vmInfo = vmManager.createAvd(new File(folder), name, target, abiType, + // skinName, sdCard, null, Boolean.parseBoolean(useSnapshot), + // true, false, NullSdkLogger.getLogger()); + + if (vmInfo == null) { + String errMsg = NLS.bind(AndroidNLS.EXC_SdkUtils_CannotCreateTheVMInstance, name); + + IStatus status = new Status(IStatus.ERROR, AndmoreAndroidPlugin.PLUGIN_ID, errMsg); + throw new CoreException(status); + } + + return vmInfo; + } + + /** + * Deletes a VM instance. + * + * @param name + * VM Name + */ + public static void deleteVm(String name) { + + AvdManager vmManager = SdkUtils.getVmManager(); + AvdInfo avdToDelete = vmManager != null ? vmManager.getAvd(name, false) : null; + if (avdToDelete != null) { + try { + if ((avdToDelete.getIniFile() != null) && avdToDelete.getIniFile().exists()) { + avdToDelete.getIniFile().delete(); + } + String path = avdToDelete.getDataFolderPath(); + if (path != null) { + File avdDir = new File(path); + if (avdDir.exists()) { + FileUtil.deleteDirRecursively(avdDir); + } + } + vmManager.removeAvd(avdToDelete); + + } catch (Exception e) { + AndmoreLogger.error("Could not delete AVD: " + e.getMessage()); //$NON-NLS-1$ + } + } + + } + + /** + * Get the reference to the File that points to the filesystem location of + * the directory where the user data files of the VM with the given name are + * stored. + * + * @param vmName + * name of the VM whose userdata directory is to be retrieved. + * @return the File object that references the filesystem location of the + * directory where the userdata files of the given VM will be + * stored. Returns a null reference if SDK is not configured or if + * there is no VM with the given name. + */ + public static File getUserdataDir(String vmName) { + AvdInfo vminfo = SdkUtils.getValidVm(vmName); + File userdataDir = null; + + if (vminfo != null) { + String vmpath = vminfo.getDataFolderPath(); + userdataDir = new File(vmpath); + } + + return userdataDir; + } + + /** + * Get the reference to the File that points to the filesystem location + * where the user data file of the VM with the given name is. + * + * @param vmName + * name of the VM whose userdata file is to be retrieved. + * @return the File object that references the filesystem location where the + * userdata of the given VM should be. Returns a null reference if + * SDK is not configured or if there is no VM with the given name. + */ + public static File getUserdataFile(String vmName) { + File userdataDir = getUserdataDir(vmName); + File userdataFile = null; + + if (userdataDir != null) { + userdataFile = new File(userdataDir, USERIMAGE_FILENAME); + } + + return userdataFile; + } + + /** + * Get the reference to the Files that point to the filesystem location + * where the state data files of the VM with the given name are. + * + * @param vmName + * name of the VM whose state data files is to be retrieved. + * @return the File objects that reference the filesystem location where the + * state data files of the given VM should be. Returns a null + * reference if SDK is not configured or if there is no VM with the + * given name. + */ + public static List getStateDataFiles(String vmName) { + File userdataDir = getUserdataDir(vmName); + List stateDataFiles = null; + + if (userdataDir != null) { + stateDataFiles = new ArrayList(); + + for (int i = 0; i < STATEDATA_FILENAMES.length; i++) { + stateDataFiles.add(new File(userdataDir, STATEDATA_FILENAMES[i])); + } + } + + return stateDataFiles; + } + + /** + * Retrieves all sample applications from a target + * + * @param target + * The target + * @return all sample applications from a target + */ + public static Object[] getSamples(IAndroidTarget target) { + List samples = new ArrayList(); + File samplesFolder = new File(target.getPath(IAndroidTarget.SAMPLES)); + samples = findSamples(samplesFolder, target); + return samples.toArray(); + } + + /** + * Find the samples inside an specific directory (recursively) + * + * @param folder + * the folder that can contain samples + * @param target + * the target of the samples in the folder + * @return a list of samples + */ + private static List findSamples(File folder, IAndroidTarget target) { + + List samples = new ArrayList(); + + if (folder.exists() && folder.isDirectory()) { + for (File sampleFolder : folder.listFiles()) { + if (sampleFolder.isDirectory()) { + if (Sample.isSample(sampleFolder)) { + samples.add(new Sample(sampleFolder, target)); + } else { + samples.addAll(findSamples(sampleFolder, target)); + } + } + } + } + + return samples; + } + + /** + * Retrieves all targets for a given SDK + * + * @param sdk + * The sdk + * + * @return all targets for the given SDK + */ + public static Object[] getTargets(Sdk sdk) { + Object[] targets = null; + if (sdk != null) { + targets = sdk.getTargets().toArray(); + } + return targets; + } + + /** + * Associates a project to a target + * + * @param project + * The project + * @param target + * The target + */ + public static void associate(IProject project, IAndroidTarget target) { + try { + Sdk.getCurrent().initProject(project, target); + } catch (Exception e) { + AndmoreLogger.error(SdkUtils.class, "Error associating project " + project.getName() //$NON-NLS-1$ + + " with target " + target.getName()); //$NON-NLS-1$ + } + } + + /** + * Retrieves the target for a project + * + * @param project + * the project + * + * @return the target for the project + */ + public static IAndroidTarget getTarget(IProject project) { + IAndroidTarget target = null; + if (project != null) { + target = Sdk.getCurrent().getTarget(project); + } + return target; + } + + /** + * Retrieves the target for a project + * + * @param project + * the project + * + * @return the target for the project + */ + public static String getMinSdkVersion(IProject project) { + String minSdkVersion = ""; + try { + AndroidManifestFile androidManifestFile = AndroidProjectManifestFile.getFromProject(project); + UsesSDKNode usesSdkNode = (UsesSDKNode) androidManifestFile.getNode(AndroidManifestNode.NodeType.UsesSdk); + if (usesSdkNode != null) { + minSdkVersion = usesSdkNode.getMinSdkVersion(); + } + + } catch (AndroidException e) { + AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); + } catch (CoreException e) { + AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); + } + return minSdkVersion; + } + + /** + * Retrieves all Activity Actions for a project. + * + * @param project + * The project + * + * @return all Activity Actions for the project. + */ + public static String[] getActivityActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "activity"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Service Actions for a project. + * + * @param project + * The project + * + * @return all Service Actions for the project. + */ + public static String[] getServiceActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "service"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Broadcast Receiver Actions for a project. + * + * @param project + * The project + * + * @return all Broadcast Receiver Actions for the project. + */ + public static String[] getReceiverActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "receiver"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Intent Filter Actions for a project. + * + * @param project + * The project + * + * @return all Intent Filter Actions for the project. + */ + public static String[] getIntentFilterCategories(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("category", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + return attributeValues; + } + + /** + * Get the api version number for a given project + * + * @param project + * : the project + * @return the api version number or 0 if some error occurs + */ + public static int getApiVersionNumberForProject(IProject project) { + int api = 0; + IAndroidTarget target = SdkUtils.getTarget(project); + if (target != null) { + AndroidVersion version = target.getVersion(); + if (version != null) { + api = version.getApiLevel(); + } + } + return api; + } + + public static String getTargetNameForProject(IProject project) { + IAndroidTarget target = getTarget(project); + return target != null ? target.getName() : ""; //$NON-NLS-1$ + } + + public static boolean isPlatformTarget(String avdName) { + IAndroidTarget target = getCurrentSdk().getAndroidTargetFor(getValidVm(avdName)); + return target != null ? target.isPlatform() : false; + } + + public static boolean isProjectTargetAPlatform(IProject project) { + IAndroidTarget target = getTarget(project); + return target != null ? target.isPlatform() : false; + } + + public static boolean isPlatformTarget(IAndroidTarget target) { + return target != null ? target.isPlatform() : false; + } + + /** + * Retrieves the APK configurations of a project + * + * @param project + * the project + * @return the APK configurations + */ + public static Map getAPKConfigurationsForProject(IProject project) { + + Map apkConfigurations = null; + + if ((project != null) && project.isOpen()) { + Sdk.getCurrent(); + // This is not supported on ADT 14 preview so let's comment it for + // now. + // apkConfigurations = + // Sdk.getProjectState(project).getApkSettings().getLocaleFilters(); + apkConfigurations = new HashMap(0); + } + + return apkConfigurations; + + } + + public static String getBaseTarget(String name) { + IAndroidTarget target = getCurrentSdk().getAndroidTargetFor(getValidVm(name)); + while (!target.isPlatform()) { + target = target.getParent(); + } + return target.getName(); + } + + /** + * Check if an SDK is an OPhone Sdk + * + * @return + */ + public static boolean isOphoneSDK() { + boolean result = false; + + // check if the folder contains the oms jar + FilenameFilter omsFilenameFilter = new FilenameFilter() { + @Override + public boolean accept(File arg0, String arg1) { + return arg1.equals(IAndroidConstants.OPHONE_JAR); + } + }; + + for (IAndroidTarget target : getCurrentSdk().getTargets()) { + File folder = new File(target.getLocation()); + if (folder.list(omsFilenameFilter).length > 0) { + result = true; + break; + } + } + + return result; + } + + /** + * Check if an SDK is an JIL sdk + * + * @return + */ + public static boolean isJILSdk() { + boolean result = false; + + // check if the folder contains the oms jar + FilenameFilter jilFilenameFilter = new FilenameFilter() { + + @Override + public boolean accept(File arg0, String arg1) { + return arg1.equals(IAndroidConstants.JIL_JAR); + } + }; + + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + for (IAndroidTarget target : sdk.getTargets()) { + File folder = new File(target.getLocation()); + if (folder.list(jilFilenameFilter).length > 0) { + result = true; + break; + } + } + } + + return result; + } + + public static String getEmulatorWindowName(String avdName, int port) { + String windowName = ""; //$NON-NLS-1$ + if (isJILSdk()) { + windowName = "JIL Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else if (isOphoneSDK()) { + windowName = "OPhone Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else { + windowName = port + ":" + avdName; //$NON-NLS-1$ + } + return windowName; + } + + public static boolean isLibraryProject(IProject project) { + return Sdk.getProjectState(project) != null ? Sdk.getProjectState(project).isLibrary() : false; + } + + /** + * Returns all available permissions + * + * @return String array containing the available permissions + */ + public static String[] getIntentFilterPermissions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("uses-permission", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + return attributeValues; + } + + /** + * Try to repair an AVD. Currently only avds with wrong image path are + * repariable. Display a message with the changes to the config.ini + * + * @param avdInfo + * @return Status ERROR if an IO exception occured. + */ + public static IStatus repairAvd(AvdInfo avdInfo) { + IStatus status = Status.OK_STATUS; + + AvdManager avdManager = Sdk.getCurrent().getAvdManager(); + Display display = PlatformUI.getWorkbench().getDisplay(); + ILogger log = new MessageBoxLog(String.format("Result of updating AVD '%s':", avdInfo.getName()), //$NON-NLS-1$ + display, false); + try { + avdManager.updateAvd(avdInfo, avdInfo.getProperties()); + // display the result + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(true); + } + SdkUtils.reloadAvds(); + + } catch (IOException e) { + status = new Status(IStatus.ERROR, AndroidPlugin.PLUGIN_ID, AndroidNLS.SdkUtils_COULD_NOT_REPAIR_AVD, e); + } + + return status; + } + + public static String getDefaultSkin(String targetName) { + IAndroidTarget target = getTargetByName(targetName); + target.getDefaultSkin().getName(); + return target != null ? target.getDefaultSkin().getName() : "HVGA"; + } + + /** + * Returns the full absolute OS path to a skin specified by name for a given + * target. + * + * @param skinName + * The name of the skin to find. Case-sensitive. + * @param target + * The target where to find the skin. + * @return a {@link File} that may or may not actually exist. + */ + public static File getSkinFolder(String skinName, IAndroidTarget target) { + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + + if (skin.exists() == false && target.isPlatform() == false) { + target = target.getParent(); + + path = target.getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + return skin; + } +} diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java index 3d730c24..0266e1ac 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java @@ -317,12 +317,12 @@ private static ProjectProperties getProjectProperties(IProject project) { String projectLocation = project.getLocation().toOSString(); // legacy support: look for default.properties - ProjectProperties properties = ProjectProperties.load(projectLocation, - PropertyType.LEGACY_DEFAULT); - if(properties == null) { - properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT); - } + //ProjectProperties properties = ProjectProperties.load(projectLocation, + // PropertyType.LEGACY_DEFAULT); + //if(properties == null) { + // properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT); + //} - return properties; + return ProjectProperties.load(projectLocation, PropertyType.PROJECT); } } diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java index 9ebf898d..ec2a397f 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java @@ -1,316 +1,316 @@ -/* - * 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 org.eclipse.andmore.android.wizards.elements; - -import org.eclipse.andmore.android.AndroidPlugin; -import org.eclipse.andmore.android.SdkUtils; -import org.eclipse.andmore.android.i18n.AndroidNLS; -import org.eclipse.andmore.android.model.AndroidProject; -import org.eclipse.andmore.android.model.IWizardModel; -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.events.SelectionEvent; -import org.eclipse.swt.events.SelectionListener; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.Rectangle; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Event; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.Table; -import org.eclipse.swt.widgets.TableColumn; -import org.eclipse.swt.widgets.TableItem; - -import com.android.sdklib.IAndroidTarget; - -/** - * SDK Selector for New Android Project Wizards - */ -public class SdkTargetSelector extends Composite { - private Table table; - - private Label mDescription; - - final private AndroidProject project; - - private IAndroidTarget selection = null; - - /** - * A selection listener that will check/uncheck items when they are - * double-clicked - */ - private final SelectionListener listener = new SelectionListener() { - /** Default selection means double-click on "most" platforms */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - if (e.item instanceof TableItem) { - TableItem i = (TableItem) e.item; - i.setChecked(!i.getChecked()); - enforceSingleSelection(i); - updateDescription(i); - IAndroidTarget newSelection = getSelection(); - project.setSdkTarget(newSelection); - - if (newSelection != null) { - project.setMinSdkVersion(getSelection().getVersion().getApiString()); - } - notifyListeners(IWizardModel.MODIFIED, new Event()); - } - - } - - @Override - public void widgetSelected(SelectionEvent e) { - if (e.item instanceof TableItem) { - TableItem i = (TableItem) e.item; - enforceSingleSelection(i); - updateDescription(i); - IAndroidTarget newSelection = getSelection(); - project.setSdkTarget(newSelection); - selection = newSelection; - project.setSample(null); - - /* - * if ((newSelection != null) && - * !selection.getFullName().equals(newSelection.getFullName())) - * { - * - * } - */ - - notifyListeners(IWizardModel.MODIFIED, new Event()); - } - - } - - /** - * If we're not in multiple selection mode, uncheck all other items when - * this one is selected. - */ - private void enforceSingleSelection(TableItem item) { - if (item.getChecked()) { - Table parentTable = item.getParent(); - for (TableItem i2 : parentTable.getItems()) { - if ((i2 != item) && i2.getChecked()) { - i2.setChecked(false); - } - } - } - } - }; - - /** - * Table Tool Tip Listener - */ - private final Listener toolTipListener = new Listener() { - @Override - public void handleEvent(Event event) { - switch (event.type) { - case SWT.MouseHover: - updateDescription(table.getItem(new Point(event.x, event.y))); - break; - case SWT.Selection: - if (event.item instanceof TableItem) { - updateDescription((TableItem) event.item); - } - break; - default: - return; - } - } - }; - - /** - * Creates a new SDK Target Selector. - * - * @param parent - * The parent composite where the selector will be added. - * @param project - * the android project - */ - public SdkTargetSelector(Composite parent, AndroidProject project) { - super(parent, SWT.NONE); - this.project = project; - - createContents(parent); - } - - /** - * Create Contents - * - * @param parent - */ - private void createContents(Composite parent) { - setLayout(new GridLayout()); - setLayoutData(new GridData(GridData.FILL_BOTH)); - setFont(parent.getFont()); - - table = new Table(this, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); - table.setHeaderVisible(true); - table.setLinesVisible(false); - table.setLayoutData(new GridData(GridData.FILL_BOTH)); - - mDescription = new Label(this, SWT.WRAP); - mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - createColumns(table); - table.addSelectionListener(listener); - fillTable(); - setupTooltip(table); - } - - /** - * Create Table Columns - * - * @param table - */ - private void createColumns(final Table table) { - // create the table columns - final TableColumn nameColumn = new TableColumn(table, SWT.NONE); - nameColumn.setText(AndroidNLS.UI_SdkTargetSelector_SdkTargetNameColumn); - final TableColumn vendorColumn = new TableColumn(table, SWT.NONE); - vendorColumn.setText(AndroidNLS.UI_SdkTargetSelector_VendorNameColumn); - final TableColumn apiColumn = new TableColumn(table, SWT.NONE); - apiColumn.setText(AndroidNLS.UI_SdkTargetSelector_APILevelColumn); - final TableColumn sdkColumn = new TableColumn(table, SWT.NONE); - sdkColumn.setText(AndroidNLS.UI_SdkTargetSelector_SDKVersionColumn); - - table.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Rectangle r = table.getClientArea(); - nameColumn.setWidth((r.width * 25) / 100); // 25% - vendorColumn.setWidth((r.width * 50) / 100); // 50% - apiColumn.setWidth((r.width * 15) / 100); // 15% - sdkColumn.setWidth((r.width * 10) / 100); // 10% - } - }); - } - - /** - * Return table selection. - * - * @return - */ - protected IAndroidTarget getSelection() { - IAndroidTarget selectedItem = null; - for (TableItem item : table.getItems()) { - Object data = item.getData(); - if (item.getChecked() && (data instanceof IAndroidTarget)) { - selectedItem = (IAndroidTarget) data; - break; - } - } - return selectedItem; - } - - /** - * Fills the table with all SDK targets. - */ - private void fillTable() { - // get the targets from the sdk - IAndroidTarget[] targets = null; - if (SdkUtils.getCurrentSdk() != null) { - targets = SdkUtils.getAllTargets(); - } else { - final Runnable listener = new Runnable() { - @Override - public void run() { - table.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - table.removeAll(); - fillTable(); - AndroidPlugin.getDefault().removeSDKLoaderListener(this); - } - }); - } - }; - AndroidPlugin.getDefault().addSDKLoaderListener(listener); - table.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - AndroidPlugin.getDefault().removeSDKLoaderListener(listener); - } - }); - } - - if ((targets != null) && (targets.length > 0)) { - table.setEnabled(true); - for (IAndroidTarget target : targets) { - TableItem item = new TableItem(table, SWT.NONE); - item.setData(target); - item.setText(0, target.getName()); - item.setText(1, target.getVendor()); - item.setText(2, target.getVersion().getApiString()); - item.setText(3, target.getVersionName()); - if (target == project.getSdkTarget()) { - item.setChecked(true); - selection = target; - } - } - } else { - table.setEnabled(false); - TableItem item = new TableItem(table, SWT.NONE); - item.setData(null); - item.setText(0, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - item.setText(1, AndroidNLS.UI_SdkTargetSelector_NoTargetAvailable); - item.setText(2, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - item.setText(3, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - } - } - - /** - * Add Tool tip for table - * - * @param table - */ - private void setupTooltip(final Table table) { - table.addListener(SWT.Dispose, toolTipListener); - table.addListener(SWT.MouseHover, toolTipListener); - table.addListener(SWT.MouseMove, toolTipListener); - table.addListener(SWT.KeyDown, toolTipListener); - } - - /** - * Updates the description label - */ - private void updateDescription(TableItem item) { - if (item != null) { - Object data = item.getData(); - if (data instanceof IAndroidTarget) { - String newTooltip = ((IAndroidTarget) data).getDescription(); - mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ - } - } else { - mDescription.setText(""); - } - } - - @Override - public void setEnabled(boolean enabled) { - if (this.getEnabled() != enabled) { - table.setEnabled(enabled); - mDescription.setEnabled(enabled); - super.setEnabled(enabled); - } - } -} +/* + * 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 org.eclipse.andmore.android.wizards.elements; + +import org.eclipse.andmore.android.AndroidPlugin; +import org.eclipse.andmore.android.SdkUtils; +import org.eclipse.andmore.android.i18n.AndroidNLS; +import org.eclipse.andmore.android.model.AndroidProject; +import org.eclipse.andmore.android.model.IWizardModel; +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.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import com.android.sdklib.IAndroidTarget; + +/** + * SDK Selector for New Android Project Wizards + */ +public class SdkTargetSelector extends Composite { + private Table table; + + private Label mDescription; + + final private AndroidProject project; + + private IAndroidTarget selection = null; + + /** + * A selection listener that will check/uncheck items when they are + * double-clicked + */ + private final SelectionListener listener = new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + IAndroidTarget newSelection = getSelection(); + project.setSdkTarget(newSelection); + + if (newSelection != null) { + project.setMinSdkVersion(getSelection().getVersion().getApiString()); + } + notifyListeners(IWizardModel.MODIFIED, new Event()); + } + + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + IAndroidTarget newSelection = getSelection(); + project.setSdkTarget(newSelection); + selection = newSelection; + project.setSample(null); + + /* + * if ((newSelection != null) && + * !selection.getFullName().equals(newSelection.getFullName())) + * { + * + * } + */ + + notifyListeners(IWizardModel.MODIFIED, new Event()); + } + + } + + /** + * If we're not in multiple selection mode, uncheck all other items when + * this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if ((i2 != item) && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }; + + /** + * Table Tool Tip Listener + */ + private final Listener toolTipListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + default: + return; + } + } + }; + + /** + * Creates a new SDK Target Selector. + * + * @param parent + * The parent composite where the selector will be added. + * @param project + * the android project + */ + public SdkTargetSelector(Composite parent, AndroidProject project) { + super(parent, SWT.NONE); + this.project = project; + + createContents(parent); + } + + /** + * Create Contents + * + * @param parent + */ + private void createContents(Composite parent) { + setLayout(new GridLayout()); + setLayoutData(new GridData(GridData.FILL_BOTH)); + setFont(parent.getFont()); + + table = new Table(this, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + table.setHeaderVisible(true); + table.setLinesVisible(false); + table.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mDescription = new Label(this, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createColumns(table); + table.addSelectionListener(listener); + fillTable(); + setupTooltip(table); + } + + /** + * Create Table Columns + * + * @param table + */ + private void createColumns(final Table table) { + // create the table columns + final TableColumn nameColumn = new TableColumn(table, SWT.NONE); + nameColumn.setText(AndroidNLS.UI_SdkTargetSelector_SdkTargetNameColumn); + final TableColumn vendorColumn = new TableColumn(table, SWT.NONE); + vendorColumn.setText(AndroidNLS.UI_SdkTargetSelector_VendorNameColumn); + final TableColumn apiColumn = new TableColumn(table, SWT.NONE); + apiColumn.setText(AndroidNLS.UI_SdkTargetSelector_APILevelColumn); + final TableColumn sdkColumn = new TableColumn(table, SWT.NONE); + sdkColumn.setText(AndroidNLS.UI_SdkTargetSelector_SDKVersionColumn); + + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + nameColumn.setWidth((r.width * 25) / 100); // 25% + vendorColumn.setWidth((r.width * 50) / 100); // 50% + apiColumn.setWidth((r.width * 15) / 100); // 15% + sdkColumn.setWidth((r.width * 10) / 100); // 10% + } + }); + } + + /** + * Return table selection. + * + * @return + */ + protected IAndroidTarget getSelection() { + IAndroidTarget selectedItem = null; + for (TableItem item : table.getItems()) { + Object data = item.getData(); + if (item.getChecked() && (data instanceof IAndroidTarget)) { + selectedItem = (IAndroidTarget) data; + break; + } + } + return selectedItem; + } + + /** + * Fills the table with all SDK targets. + */ + private void fillTable() { + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (SdkUtils.getCurrentSdk() != null) { + targets = SdkUtils.getAllTargets(); + } else { + final Runnable listener = new Runnable() { + @Override + public void run() { + table.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + table.removeAll(); + fillTable(); + AndroidPlugin.getDefault().removeSDKLoaderListener(this); + } + }); + } + }; + AndroidPlugin.getDefault().addSDKLoaderListener(listener); + table.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + AndroidPlugin.getDefault().removeSDKLoaderListener(listener); + } + }); + } + + if ((targets != null) && (targets.length > 0)) { + table.setEnabled(true); + for (IAndroidTarget target : targets) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(target); + item.setText(0, target.getName()); + item.setText(1, target.getVendor()); + item.setText(2, target.getVersion().getApiString()); + item.setText(3, target.getVersionName() == null ? target.getVersion().getApiString() : target.getVersionName()); + if (target == project.getSdkTarget()) { + item.setChecked(true); + selection = target; + } + } + } else { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + item.setText(1, AndroidNLS.UI_SdkTargetSelector_NoTargetAvailable); + item.setText(2, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + item.setText(3, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + } + } + + /** + * Add Tool tip for table + * + * @param table + */ + private void setupTooltip(final Table table) { + table.addListener(SWT.Dispose, toolTipListener); + table.addListener(SWT.MouseHover, toolTipListener); + table.addListener(SWT.MouseMove, toolTipListener); + table.addListener(SWT.KeyDown, toolTipListener); + } + + /** + * Updates the description label + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof IAndroidTarget) { + String newTooltip = ((IAndroidTarget) data).getDescription(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } else { + mDescription.setText(""); + } + } + + @Override + public void setEnabled(boolean enabled) { + if (this.getEnabled() != enabled) { + table.setEnabled(enabled); + mDescription.setEnabled(enabled); + super.setEnabled(enabled); + } + } +} diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java index c7c57b9c..217cea1a 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java @@ -1,129 +1,130 @@ -/* - * 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 org.eclipse.andmore.android.wizards.project; - -import org.eclipse.andmore.android.SdkUtils; -import org.eclipse.andmore.android.model.AndroidProject; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.jface.viewers.ITreeContentProvider; -import org.eclipse.jface.viewers.Viewer; - -import com.android.sdklib.IAndroidTarget; - -/** - * Class that implements a content provider for the Samples Tree viewers. - */ -@SuppressWarnings("restriction") -class TreeContentProvider implements ITreeContentProvider { - AndroidProject project = null; - - public TreeContentProvider(AndroidProject project) { - this.project = project; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang. - * Object) - */ - @Override - public Object[] getChildren(Object arg0) { - Object[] objects; - - if (arg0 instanceof IAndroidTarget) { - objects = SdkUtils.getSamples((IAndroidTarget) arg0); - } else { - objects = new Object[0]; - } - return objects; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object - * ) - */ - @Override - public Object getParent(Object arg0) { - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang. - * Object) - */ - @Override - public boolean hasChildren(Object arg0) { - Object[] obj = getChildren(arg0); - return obj == null ? false : obj.length > 0; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java - * .lang.Object) - */ - @Override - public Object[] getElements(Object arg0) { - Object[] objs = null; - - if (arg0 instanceof Sdk) { - Sdk sdk = (Sdk) arg0; - Object[] targets = SdkUtils.getTargets(sdk); - if (targets.length > 0) { - for (IAndroidTarget target : (IAndroidTarget[]) targets) { - if (target.equals(project.getSdkTarget())) { - objs = SdkUtils.getSamples(target); - } - } - } else { - objs = new Object[0]; - } - } - return objs; - } - - /* - * (non-Javadoc) - * - * @see org.eclipse.jface.viewers.IContentProvider#dispose() - */ - @Override - public void dispose() { - // do nothing - } - - /* - * (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 arg0, Object arg1, Object arg2) { - // do nothing - } -} +/* + * 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 org.eclipse.andmore.android.wizards.project; + +import org.eclipse.andmore.android.SdkUtils; +import org.eclipse.andmore.android.model.AndroidProject; +import org.eclipse.andmore.internal.sdk.Sdk; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import com.android.sdklib.IAndroidTarget; + +/** + * Class that implements a content provider for the Samples Tree viewers. + */ +@SuppressWarnings("restriction") +class TreeContentProvider implements ITreeContentProvider { + AndroidProject project = null; + + public TreeContentProvider(AndroidProject project) { + this.project = project; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang. + * Object) + */ + @Override + public Object[] getChildren(Object arg0) { + Object[] objects; + + if (arg0 instanceof IAndroidTarget) { + objects = SdkUtils.getSamples((IAndroidTarget) arg0); + } else { + objects = new Object[0]; + } + return objects; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object + * ) + */ + @Override + public Object getParent(Object arg0) { + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang. + * Object) + */ + @Override + public boolean hasChildren(Object arg0) { + Object[] obj = getChildren(arg0); + return obj == null ? false : obj.length > 0; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java + * .lang.Object) + */ + @Override + public Object[] getElements(Object arg0) { + Object[] objs = null; + + if (arg0 instanceof Sdk) { + Sdk sdk = (Sdk) arg0; + Object[] targets = SdkUtils.getTargets(sdk); + if (targets.length > 0) { + for (Object o : targets) { + IAndroidTarget target = (IAndroidTarget) o; + if (target.equals(project.getSdkTarget())) { + objs = SdkUtils.getSamples(target); + } + } + } else { + objs = new Object[0]; + } + } + return objs; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.jface.viewers.IContentProvider#dispose() + */ + @Override + public void dispose() { + // do nothing + } + + /* + * (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 arg0, Object arg1, Object arg2) { + // do nothing + } +} diff --git a/andmore-core/plugins/certmanager/.classpath b/andmore-core/plugins/certmanager/.classpath index 21319ace..a6c5d251 100644 --- a/andmore-core/plugins/certmanager/.classpath +++ b/andmore-core/plugins/certmanager/.classpath @@ -1,9 +1,9 @@ - - - - - - - - - + + + + + + + + + diff --git a/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF b/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF index c6ff8dd6..e3e12107 100644 --- a/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF @@ -1,32 +1,32 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.certmanager;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.certmanager.CertificateManagerActivator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.andmore.android.common, - org.junit, - org.eclipse.equinox.security, - org.eclipse.core.resources, - org.eclipse.ui.ide, - org.eclipse.core.expressions -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Bundle-ClassPath: ., - lib/bcprov-jdk15on-147.jar, - lib/bcpkix-jdk15on-147.jar -Import-Package: org.eclipse.ui.actions -Export-Package: org.eclipse.andmore.android.certmanager, - org.eclipse.andmore.android.certmanager.command, - org.eclipse.andmore.android.certmanager.core, - org.eclipse.andmore.android.certmanager.exception, - org.eclipse.andmore.android.certmanager.job, - org.eclipse.andmore.android.certmanager.packaging, - org.eclipse.andmore.android.certmanager.packaging.sign, - org.eclipse.andmore.android.certmanager.ui.model, - org.eclipse.andmore.android.certmanager.ui.wizards, - org.eclipse.andmore.android.certmanager.views +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.certmanager;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.certmanager.CertificateManagerActivator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.andmore.android.common, + org.junit, + org.eclipse.equinox.security, + org.eclipse.core.resources, + org.eclipse.ui.ide, + org.eclipse.core.expressions +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Bundle-ClassPath: ., + lib/bcprov-jdk15on-147.jar, + lib/bcpkix-jdk15on-147.jar +Import-Package: org.eclipse.ui.actions +Export-Package: org.eclipse.andmore.android.certmanager, + org.eclipse.andmore.android.certmanager.command, + org.eclipse.andmore.android.certmanager.core, + org.eclipse.andmore.android.certmanager.exception, + org.eclipse.andmore.android.certmanager.job, + org.eclipse.andmore.android.certmanager.packaging, + org.eclipse.andmore.android.certmanager.packaging.sign, + org.eclipse.andmore.android.certmanager.ui.model, + org.eclipse.andmore.android.certmanager.ui.wizards, + org.eclipse.andmore.android.certmanager.views diff --git a/andmore-core/plugins/common/.classpath b/andmore-core/plugins/common/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/common/.classpath +++ b/andmore-core/plugins/common/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/common/META-INF/MANIFEST.MF b/andmore-core/plugins/common/META-INF/MANIFEST.MF index b921b5a7..e0fed49d 100644 --- a/andmore-core/plugins/common/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/common/META-INF/MANIFEST.MF @@ -1,38 +1,38 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.common;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.common.CommonPlugin -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.ui.editors, - org.eclipse.ui.browser, - org.eclipse.text, - org.eclipse.andmore.android.logger, - org.eclipse.core.net, - org.eclipse.ui.net, - org.apache.xerces, - org.eclipse.jdt.core, - org.eclipse.ui.console, - org.eclipse.ui.ide, - org.apache.commons.httpclient -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: org.eclipse.andmore.android.common, - org.eclipse.andmore.android.common.exception, - org.eclipse.andmore.android.common.log, - org.eclipse.andmore.android.common.preferences, - org.eclipse.andmore.android.common.proxy, - org.eclipse.andmore.android.common.utilities, - org.eclipse.andmore.android.common.utilities.i18n, - org.eclipse.andmore.android.common.utilities.ui, - org.eclipse.andmore.android.manifest, - org.eclipse.andmore.android.model.manifest, - org.eclipse.andmore.android.model.manifest.dom, - org.eclipse.andmore.android.wizards, - org.eclipse.andmore.android.wizards.elements -Bundle-ClassPath: . -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.common;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.common.CommonPlugin +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.ui.editors, + org.eclipse.ui.browser, + org.eclipse.text, + org.eclipse.andmore.android.logger, + org.eclipse.core.net, + org.eclipse.ui.net, + org.apache.xerces, + org.eclipse.jdt.core, + org.eclipse.ui.console, + org.eclipse.ui.ide, + org.apache.commons.httpclient +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: org.eclipse.andmore.android.common, + org.eclipse.andmore.android.common.exception, + org.eclipse.andmore.android.common.log, + org.eclipse.andmore.android.common.preferences, + org.eclipse.andmore.android.common.proxy, + org.eclipse.andmore.android.common.utilities, + org.eclipse.andmore.android.common.utilities.i18n, + org.eclipse.andmore.android.common.utilities.ui, + org.eclipse.andmore.android.manifest, + org.eclipse.andmore.android.model.manifest, + org.eclipse.andmore.android.model.manifest.dom, + org.eclipse.andmore.android.wizards, + org.eclipse.andmore.android.wizards.elements +Bundle-ClassPath: . +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/db.core/.classpath b/andmore-core/plugins/db.core/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/db.core/.classpath +++ b/andmore-core/plugins/db.core/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/db.core/META-INF/MANIFEST.MF b/andmore-core/plugins/db.core/META-INF/MANIFEST.MF index 41bcb242..01441043 100644 --- a/andmore-core/plugins/db.core/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/db.core/META-INF/MANIFEST.MF @@ -1,32 +1,32 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.db.core;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.db.core.DbCoreActivator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.datatools.connectivity, - org.eclipse.andmore.android.common, - org.eclipse.core.resources, - org.eclipse.datatools.connectivity.sqm.core, - org.eclipse.datatools.modelbase.sql, - org.junit, - org.eclipse.datatools.sqltools.data.ui, - org.eclipse.andmore.android.codeutils, - org.eclipse.datatools.sqltools.editor.core, - org.eclipse.datatools.sqltools.result, - org.eclipse.core.expressions, - org.eclipse.datatools.sqltools.ddlgen.ui -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.db.core, - org.eclipse.andmore.android.db.core.command, - org.eclipse.andmore.android.db.core.event, - org.eclipse.andmore.android.db.core.exception, - org.eclipse.andmore.android.db.core.model, - org.eclipse.andmore.android.db.core.ui, - org.eclipse.andmore.android.db.core.ui.action, - org.eclipse.andmore.android.db.core.ui.view +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.db.core;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.db.core.DbCoreActivator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.datatools.connectivity, + org.eclipse.andmore.android.common, + org.eclipse.core.resources, + org.eclipse.datatools.connectivity.sqm.core, + org.eclipse.datatools.modelbase.sql, + org.junit, + org.eclipse.datatools.sqltools.data.ui, + org.eclipse.andmore.android.codeutils, + org.eclipse.datatools.sqltools.editor.core, + org.eclipse.datatools.sqltools.result, + org.eclipse.core.expressions, + org.eclipse.datatools.sqltools.ddlgen.ui +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.db.core, + org.eclipse.andmore.android.db.core.command, + org.eclipse.andmore.android.db.core.event, + org.eclipse.andmore.android.db.core.exception, + org.eclipse.andmore.android.db.core.model, + org.eclipse.andmore.android.db.core.ui, + org.eclipse.andmore.android.db.core.ui.action, + org.eclipse.andmore.android.db.core.ui.view diff --git a/andmore-core/plugins/db.devices/.classpath b/andmore-core/plugins/db.devices/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/db.devices/.classpath +++ b/andmore-core/plugins/db.devices/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF b/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF index fcd92cc9..675bb1ad 100644 --- a/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF @@ -1,22 +1,22 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.db.devices;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.db.devices.DbDevicesPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.andmore.android.db.core, - org.eclipse.andmore.android, - org.eclipse.jdt.core, - org.eclipse.datatools.modelbase.sql, - org.eclipse.andmore.android.common, - org.eclipse.ui.console, - org.eclipse.datatools.sqltools.result, - org.eclipse.sequoyah.device.framework, - org.eclipse.core.resources, - org.eclipse.andmore;bundle-version="0.5.0" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.db.devices;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.db.devices.DbDevicesPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.andmore.android.db.core, + org.eclipse.andmore.android, + org.eclipse.jdt.core, + org.eclipse.datatools.modelbase.sql, + org.eclipse.andmore.android.common, + org.eclipse.ui.console, + org.eclipse.datatools.sqltools.result, + org.eclipse.sequoyah.device.framework, + org.eclipse.core.resources, + org.eclipse.andmore;bundle-version="0.5.0" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/devices.services/.classpath b/andmore-core/plugins/devices.services/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/devices.services/.classpath +++ b/andmore-core/plugins/devices.services/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF b/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF index 01132bd3..92c2f994 100644 --- a/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF @@ -1,21 +1,21 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.devices.services;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.devices.services.DeviceServicesPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.handset, - org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.common, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.ui.console, - org.eclipse.ui, - org.eclipse.core.expressions, - org.eclipse.core.runtime -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.devices.services.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.devices.services;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.devices.services.DeviceServicesPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.handset, + org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.common, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.ui.console, + org.eclipse.ui, + org.eclipse.core.expressions, + org.eclipse.core.runtime +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.devices.services.i18n diff --git a/andmore-core/plugins/emulator/.classpath b/andmore-core/plugins/emulator/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/emulator/.classpath +++ b/andmore-core/plugins/emulator/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/emulator/META-INF/MANIFEST.MF b/andmore-core/plugins/emulator/META-INF/MANIFEST.MF index 7bfdee53..376e03f9 100644 --- a/andmore-core/plugins/emulator/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/emulator/META-INF/MANIFEST.MF @@ -1,47 +1,48 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.emulator;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.emulator.EmulatorPlugin -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Require-Bundle: org.eclipse.andmore, - org.eclipse.andmore.ddms, - org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.net, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.framework.ui, - org.eclipse.sequoyah.device.framework.wizard, - org.eclipse.sequoyah.vnc.vncviewer, - org.eclipse.sequoyah.vnc.protocol, - org.eclipse.sequoyah.vnc.vncviewer.vncviews, - org.eclipse.andmore.base, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Export-Package: org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.emulator.core.devfrm, - org.eclipse.andmore.android.emulator.core.emulationui, - org.eclipse.andmore.android.emulator.core.exception, - org.eclipse.andmore.android.emulator.core.model, - org.eclipse.andmore.android.emulator.core.skin, - org.eclipse.andmore.android.emulator.core.utils, - org.eclipse.andmore.android.emulator.device, - org.eclipse.andmore.android.emulator.device.definition, - org.eclipse.andmore.android.emulator.device.handlers, - org.eclipse.andmore.android.emulator.device.instance, - org.eclipse.andmore.android.emulator.device.refresh, - org.eclipse.andmore.android.emulator.device.sync, - org.eclipse.andmore.android.emulator.i18n, - org.eclipse.andmore.android.emulator.logic, - org.eclipse.andmore.android.emulator.skin.android.parser, - org.eclipse.andmore.android.emulator.ui.view -Bundle-ClassPath: lib/jakarta-oro-2.0.8.jar, - . -Bundle-ActivationPolicy: lazy - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.emulator;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.emulator.EmulatorPlugin +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Require-Bundle: org.eclipse.andmore, + org.eclipse.andmore.ddms, + org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.net, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.framework.ui, + org.eclipse.sequoyah.device.framework.wizard, + org.eclipse.sequoyah.vnc.vncviewer, + org.eclipse.sequoyah.vnc.protocol, + org.eclipse.sequoyah.vnc.vncviewer.vncviews, + org.eclipse.andmore.swt, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8", + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2" +Export-Package: org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.emulator.core.devfrm, + org.eclipse.andmore.android.emulator.core.emulationui, + org.eclipse.andmore.android.emulator.core.exception, + org.eclipse.andmore.android.emulator.core.model, + org.eclipse.andmore.android.emulator.core.skin, + org.eclipse.andmore.android.emulator.core.utils, + org.eclipse.andmore.android.emulator.device, + org.eclipse.andmore.android.emulator.device.definition, + org.eclipse.andmore.android.emulator.device.handlers, + org.eclipse.andmore.android.emulator.device.instance, + org.eclipse.andmore.android.emulator.device.refresh, + org.eclipse.andmore.android.emulator.device.sync, + org.eclipse.andmore.android.emulator.i18n, + org.eclipse.andmore.android.emulator.logic, + org.eclipse.andmore.android.emulator.skin.android.parser, + org.eclipse.andmore.android.emulator.ui.view +Bundle-ClassPath: lib/jakarta-oro-2.0.8.jar, + . +Bundle-ActivationPolicy: lazy + diff --git a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java index 89914bd4..3b360074 100644 --- a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java +++ b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java @@ -58,6 +58,7 @@ import com.android.sdklib.IAndroidTarget; import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.targets.AndroidTargetManager; /** * DESCRIPTION: This class represents a Android Emulator instance @@ -366,7 +367,7 @@ public static void populateWithVMInfo(String instanceName, Properties instancePr if (vmInfo != null) { // VM target - instanceProperties.setProperty(IDevicePropertiesConstants.vmTarget, vmInfo.getTarget().getName()); + instanceProperties.setProperty(IDevicePropertiesConstants.vmTarget, SdkUtils.getCurrentSdk().getAndroidTargetFor(vmInfo).getName()); // ABI Type instanceProperties.setProperty(IDevicePropertiesConstants.abiType, vmInfo.getAbiType()); @@ -404,8 +405,8 @@ public File getSkinPath() { AvdInfo avdInfo = SdkUtils.getValidVm(getName()); if (avdInfo != null) { String skinPath = avdInfo.getProperties().get("skin.path"); - skinPath = SdkUtils.getCurrentSdk().getSdkOsLocation() + skinPath; - IAndroidTarget target = avdInfo.getTarget(); + skinPath = new File(SdkUtils.getCurrentSdk().getSdkFileLocation(), skinPath).getAbsolutePath(); + IAndroidTarget target = SdkUtils.getCurrentSdk().getAndroidTargetFor(avdInfo); File candidateFile = new File(skinPath); // If path specified on the skin does not exist, try to retrieve it // from the target. @@ -459,7 +460,7 @@ public IAndroidTarget getAndroidTarget() { IAndroidTarget result = null; AvdInfo avdInfo = SdkUtils.getValidVm(getName()); if (avdInfo != null) { - result = avdInfo.getTarget(); + result = SdkUtils.getCurrentSdk().getAndroidTargetFor(avdInfo); } return result; } diff --git a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java index 2a98ed18..fcef847e 100644 --- a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java +++ b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java @@ -31,6 +31,7 @@ import org.eclipse.andmore.android.emulator.device.IDevicePropertiesConstants; import org.eclipse.andmore.android.emulator.device.definition.AndroidEmuDefMgr; import org.eclipse.andmore.android.emulator.i18n.EmulatorNLS; +import org.eclipse.andmore.internal.sdk.Sdk; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.preference.IPreferenceNode; @@ -59,8 +60,12 @@ import com.android.sdklib.IAndroidTarget; import com.android.sdklib.ISystemImage; import com.android.SdkConstants; +import com.android.repository.testframework.FakeProgressIndicator; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdklib.repository.targets.SystemImageManager; /** * DESCRIPTION:
@@ -818,18 +823,24 @@ public void modifyText(ModifyEvent e) { private void populateAbiTypeCombo(Combo abiTypeCombo) { // System Images represents the ABI types - ISystemImage[] images = vmTarget.getSystemImages(); + // TODO these IdDisplay are almost certainly wrong. + Collection images = Sdk.getCurrent().getAndroidSdkHandler().getSystemImageManager(new FakeProgressIndicator()) + .lookup(IdDisplay.create(vmTarget.getName(), vmTarget.getName()), vmTarget.getVersion(), + IdDisplay.create(vmTarget.getVendor(), vmTarget.getVendor())); // in case no images are found, get try its parent - if ((images == null) || ((images.length == 0) && !vmTarget.isPlatform())) { - images = vmTarget.getParent().getSystemImages(); + if ((images == null) || ((images.size() == 0) && !vmTarget.isPlatform())) { + IAndroidTarget parent = vmTarget.getParent(); + images = Sdk.getCurrent().getAndroidSdkHandler().getSystemImageManager(new FakeProgressIndicator()).lookup( + IdDisplay.create(parent.getName(), parent.getName()), parent.getVersion(), + IdDisplay.create(parent.getVendor(), parent.getVendor())); } // always clean abi combo since it will be reloaded abiTypeCombo.removeAll(); int i = 0; - if ((images != null) && (images.length > 0)) { + if ((images != null) && (images.size() > 0)) { for (ISystemImage image : images) { String prettyAbiName = AvdInfo.getPrettyAbiType(image); abiTypeCombo.add(prettyAbiName); diff --git a/andmore-core/plugins/handset/.classpath b/andmore-core/plugins/handset/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/handset/.classpath +++ b/andmore-core/plugins/handset/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/handset/META-INF/MANIFEST.MF b/andmore-core/plugins/handset/META-INF/MANIFEST.MF index e84a4485..3780d952 100644 --- a/andmore-core/plugins/handset/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/handset/META-INF/MANIFEST.MF @@ -1,20 +1,20 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.handset;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.handset.HandsetPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.ui, - org.eclipse.ui.console, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.handset, - org.eclipse.andmore.android.handset.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.handset;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.handset.HandsetPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.ui, + org.eclipse.ui.console, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.handset, + org.eclipse.andmore.android.handset.i18n diff --git a/andmore-core/plugins/installer/.classpath b/andmore-core/plugins/installer/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/installer/.classpath +++ b/andmore-core/plugins/installer/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/installer/META-INF/MANIFEST.MF b/andmore-core/plugins/installer/META-INF/MANIFEST.MF index b454b444..206ff52d 100644 --- a/andmore-core/plugins/installer/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/installer/META-INF/MANIFEST.MF @@ -1,40 +1,40 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.installer;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.installer.InstallerPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.equinox.p2.artifact.repository, - org.eclipse.equinox.p2.core, - org.eclipse.equinox.p2.engine, - org.eclipse.equinox.p2.metadata, - org.eclipse.equinox.p2.metadata.repository, - org.eclipse.equinox.p2.ui, - org.eclipse.equinox.p2.repository, - org.eclipse.equinox.p2.operations, - org.eclipse.equinox.p2.touchpoint.natives, - org.eclipse.andmore.android.common, - org.eclipse.jdt.launching, - org.eclipse.andmore, - org.apache.commons.httpclient, - org.eclipse.ui.browser, - org.eclipse.equinox.p2.ui.sdk, - org.eclipse.andmore.base -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.installer, - org.eclipse.andmore.android.installer.i18n, - org.eclipse.andmore.android.installer.ui.dialogs, - org.eclipse.andmore.android.installer.utilities -Import-Package: org.eclipse.andmore.android.installer.utilities, - org.eclipse.core.internal.net, - org.eclipse.core.net.proxy, - org.eclipse.equinox.security.storage, - org.eclipse.jdt.core, - org.eclipse.jdt.internal.ui, - org.eclipse.ui.internal.net.auth +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.installer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.installer.InstallerPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.equinox.p2.artifact.repository, + org.eclipse.equinox.p2.core, + org.eclipse.equinox.p2.engine, + org.eclipse.equinox.p2.metadata, + org.eclipse.equinox.p2.metadata.repository, + org.eclipse.equinox.p2.ui, + org.eclipse.equinox.p2.repository, + org.eclipse.equinox.p2.operations, + org.eclipse.equinox.p2.touchpoint.natives, + org.eclipse.andmore.android.common, + org.eclipse.jdt.launching, + org.eclipse.andmore, + org.apache.commons.httpclient, + org.eclipse.ui.browser, + org.eclipse.equinox.p2.ui.sdk, + org.eclipse.andmore.base +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.installer, + org.eclipse.andmore.android.installer.i18n, + org.eclipse.andmore.android.installer.ui.dialogs, + org.eclipse.andmore.android.installer.utilities +Import-Package: org.eclipse.andmore.android.installer.utilities, + org.eclipse.core.internal.net, + org.eclipse.core.net.proxy, + org.eclipse.equinox.security.storage, + org.eclipse.jdt.core, + org.eclipse.jdt.internal.ui, + org.eclipse.ui.internal.net.auth diff --git a/andmore-core/plugins/launch/.classpath b/andmore-core/plugins/launch/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/launch/.classpath +++ b/andmore-core/plugins/launch/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/launch/META-INF/MANIFEST.MF b/andmore-core/plugins/launch/META-INF/MANIFEST.MF index e4746b8f..818eec6f 100644 --- a/andmore-core/plugins/launch/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/launch/META-INF/MANIFEST.MF @@ -1,25 +1,25 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.launch;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.launch.LaunchPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.common, - org.eclipse.debug.ui, - org.eclipse.jdt.launching, - org.eclipse.jdt.core, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.andmore.ddms, - org.eclipse.andmore, - org.eclipse.core.runtime, - org.eclipse.ui.console, - org.eclipse.ui, - org.eclipse.andmore.base -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.launch.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.launch;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.launch.LaunchPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.common, + org.eclipse.debug.ui, + org.eclipse.jdt.launching, + org.eclipse.jdt.core, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.andmore.ddms, + org.eclipse.andmore, + org.eclipse.core.runtime, + org.eclipse.ui.console, + org.eclipse.ui, + org.eclipse.andmore.swt +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.launch.i18n diff --git a/andmore-core/plugins/logger.collector/.classpath b/andmore-core/plugins/logger.collector/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/logger.collector/.classpath +++ b/andmore-core/plugins/logger.collector/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF b/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF index e66ba7f8..f2ffb600 100644 --- a/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF @@ -1,14 +1,14 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.logger.collector;singleton:=true -Bundle-Version: 0.5.2.qualifier -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime -Bundle-ActivationPolicy: lazy -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.logger.collector.core -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Import-Package: org.eclipse.andmore.android.common.utilities, - org.eclipse.andmore.android.logger +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.logger.collector;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime +Bundle-ActivationPolicy: lazy +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.logger.collector.core +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Import-Package: org.eclipse.andmore.android.common.utilities, + org.eclipse.andmore.android.logger diff --git a/andmore-core/plugins/logger/.classpath b/andmore-core/plugins/logger/.classpath index be9ef572..13f61b0e 100644 --- a/andmore-core/plugins/logger/.classpath +++ b/andmore-core/plugins/logger/.classpath @@ -1,8 +1,8 @@ - - - - - - - - + + + + + + + + diff --git a/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/logger/META-INF/MANIFEST.MF b/andmore-core/plugins/logger/META-INF/MANIFEST.MF index 7feb2958..45349b74 100644 --- a/andmore-core/plugins/logger/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/logger/META-INF/MANIFEST.MF @@ -1,13 +1,13 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Andmore Android Logger -Bundle-SymbolicName: org.eclipse.andmore.android.logger;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.logger.internal.Activator -Bundle-Vendor: Eclipse Andmore -Export-Package: org.eclipse.andmore.android.logger -Bundle-ClassPath: lib/log4j-1.2.14.jar, - . -Require-Bundle: org.eclipse.core.runtime -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Andmore Android Logger +Bundle-SymbolicName: org.eclipse.andmore.android.logger;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.logger.internal.Activator +Bundle-Vendor: Eclipse Andmore +Export-Package: org.eclipse.andmore.android.logger +Bundle-ClassPath: lib/log4j-1.2.14.jar, + . +Require-Bundle: org.eclipse.core.runtime +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/mat/.classpath b/andmore-core/plugins/mat/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/mat/.classpath +++ b/andmore-core/plugins/mat/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/mat/META-INF/MANIFEST.MF b/andmore-core/plugins/mat/META-INF/MANIFEST.MF index cf28776a..9372c4b5 100644 --- a/andmore-core/plugins/mat/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/mat/META-INF/MANIFEST.MF @@ -1,21 +1,21 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.mat;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.mat.Activator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.mat.ui, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.core.resources, - org.eclipse.andmore.android, - org.eclipse.andmore.android.emulator -Bundle-ActivationPolicy: lazy -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.mat.i18n -Import-Package: org.eclipse.andmore.android.common.log - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.mat;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.mat.Activator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.mat.ui, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.core.resources, + org.eclipse.andmore.android, + org.eclipse.andmore.android.emulator +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.mat.i18n +Import-Package: org.eclipse.andmore.android.common.log + diff --git a/andmore-core/plugins/packaging.ui/.classpath b/andmore-core/plugins/packaging.ui/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/packaging.ui/.classpath +++ b/andmore-core/plugins/packaging.ui/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF b/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF index 4a497ac9..872ee7a3 100644 --- a/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF @@ -1,22 +1,22 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.packaging.ui;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.packaging.ui.PackagingUIPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.core.resources, - org.eclipse.core.runtime, - org.eclipse.ui, - org.eclipse.ui.forms, - org.eclipse.ui.ide, - org.eclipse.ui.views, - org.eclipse.jdt.core, - org.eclipse.jdt.ui, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android, - org.eclipse.andmore.android.certmanager -Bundle-Localization: plugin -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.packaging.ui.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.packaging.ui;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.packaging.ui.PackagingUIPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.core.resources, + org.eclipse.core.runtime, + org.eclipse.ui, + org.eclipse.ui.forms, + org.eclipse.ui.ide, + org.eclipse.ui.views, + org.eclipse.jdt.core, + org.eclipse.jdt.ui, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android, + org.eclipse.andmore.android.certmanager +Bundle-Localization: plugin +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.packaging.ui.i18n diff --git a/andmore-core/plugins/remote.device/.classpath b/andmore-core/plugins/remote.device/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/remote.device/.classpath +++ b/andmore-core/plugins/remote.device/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF b/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF index dda83e9f..a13cc697 100644 --- a/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF @@ -1,20 +1,20 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.remote;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.remote.RemoteDevicePlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.framework.ui, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.remote, - org.eclipse.andmore.android.remote.i18n, - org.eclipse.andmore.android.remote.instance +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.remote;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.remote.RemoteDevicePlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.framework.ui, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.remote, + org.eclipse.andmore.android.remote.i18n, + org.eclipse.andmore.android.remote.instance diff --git a/andmore-core/plugins/snippets/.classpath b/andmore-core/plugins/snippets/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/snippets/.classpath +++ b/andmore-core/plugins/snippets/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/snippets/META-INF/MANIFEST.MF b/andmore-core/plugins/snippets/META-INF/MANIFEST.MF index 3b9eddc8..47be168a 100644 --- a/andmore-core/plugins/snippets/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/snippets/META-INF/MANIFEST.MF @@ -1,19 +1,19 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.codesnippets;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Localization: plugin -Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.ui.editors, - org.eclipse.gef, - org.eclipse.jface.text, - org.eclipse.jdt.ui, - org.eclipse.wst.common.snippets, - org.eclipse.andmore.android.common -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: org.eclipse.andmore.android.codesnippets.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.codesnippets;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Localization: plugin +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.editors, + org.eclipse.gef, + org.eclipse.jface.text, + org.eclipse.jdt.ui, + org.eclipse.wst.common.snippets, + org.eclipse.andmore.android.common +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: org.eclipse.andmore.android.codesnippets.i18n diff --git a/andmore-core/plugins/translation/.classpath b/andmore-core/plugins/translation/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/translation/.classpath +++ b/andmore-core/plugins/translation/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/translation/META-INF/MANIFEST.MF b/andmore-core/plugins/translation/META-INF/MANIFEST.MF index 7a4f1992..4e57a860 100644 --- a/andmore-core/plugins/translation/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/translation/META-INF/MANIFEST.MF @@ -1,26 +1,26 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.translation;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Require-Bundle: org.eclipse.core.net, - org.eclipse.core.runtime, - org.eclipse.jface, - org.eclipse.sequoyah.localization.tools, - org.eclipse.ui.workbench, - org.eclipse.andmore.android.common, - org.eclipse.ui.net, - org.eclipse.sequoyah.localization.android, - org.eclipse.sequoyah.localization.editor, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Bundle-ClassPath: . -Import-Package: org.apache.commons.httpclient, - org.apache.commons.httpclient.auth;version="3.1.0", - org.apache.commons.httpclient.methods;version="3.1.0", - org.apache.commons.httpclient.params;version="3.1.0" -Bundle-Activator: org.eclipse.andmore.android.localization.translators.TranslationPlugin -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.translation;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.net, + org.eclipse.core.runtime, + org.eclipse.jface, + org.eclipse.sequoyah.localization.tools, + org.eclipse.ui.workbench, + org.eclipse.andmore.android.common, + org.eclipse.ui.net, + org.eclipse.sequoyah.localization.android, + org.eclipse.sequoyah.localization.editor, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8" +Bundle-ClassPath: . +Import-Package: org.apache.commons.httpclient, + org.apache.commons.httpclient.auth;version="3.1.0", + org.apache.commons.httpclient.methods;version="3.1.0", + org.apache.commons.httpclient.params;version="3.1.0" +Bundle-Activator: org.eclipse.andmore.android.localization.translators.TranslationPlugin +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy diff --git a/andmore-swt/.gitignore b/andmore-swt/.gitignore new file mode 100644 index 00000000..d16920c5 --- /dev/null +++ b/andmore-swt/.gitignore @@ -0,0 +1,14 @@ +*~ +*.bak +*.pyc +Thumbs.db +*.class +*.DS_Store +.gradle +/build +/out +/repo +.idea/workspace.xml +.idea/dictionaries/tnorbye.xml +bin + diff --git a/andmore-swt/.project b/andmore-swt/.project new file mode 100644 index 00000000..3d52c9a2 --- /dev/null +++ b/andmore-swt/.project @@ -0,0 +1,17 @@ + + + andmore-parent + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/andmore-swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project new file mode 100644 index 00000000..555fa175 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.ddmuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml new file mode 100644 index 00000000..ee4da604 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Dalvik Debug Monitor Service + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml new file mode 100644 index 00000000..f489559b --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.ddmuilib.feature + eclipse-feature + ddmuilib + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.ddmuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project new file mode 100644 index 00000000..7e0e0583 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.hierarchyviewer2lib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.properties b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml new file mode 100644 index 00000000..56959b91 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml @@ -0,0 +1,53 @@ + + + + + Android UI Hierarchy Viewer2 library + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml new file mode 100644 index 00000000..08a4a37e --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.hierarchyviewer2lib.feature + eclipse-feature + hierarchyviewer2lib + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.project b/andmore-swt/features/org.eclipse.andmore.sdkstats/.project new file mode 100644 index 00000000..5d7e617d --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.sdkstats.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties b/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.properties b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml new file mode 100644 index 00000000..e05be3a7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI SDK Statistics + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml new file mode 100644 index 00000000..7c24d460 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.sdkstats.feature + eclipse-feature + sdkstats + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkstats/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project new file mode 100644 index 00000000..909e7c2c --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.sdkuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml new file mode 100644 index 00000000..2c1fe365 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI SDK LIbrary + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml new file mode 100644 index 00000000..2858670e --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.sdkuilib.feature + eclipse-feature + sdkuilib + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.gitignore b/andmore-swt/features/org.eclipse.andmore.swt/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.project b/andmore-swt/features/org.eclipse.andmore.swt/.project new file mode 100644 index 00000000..57952847 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.swt.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.swt/build.properties b/andmore-swt/features/org.eclipse.andmore.swt/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.swt/feature.properties b/andmore-swt/features/org.eclipse.andmore.swt/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swt/feature.xml b/andmore-swt/features/org.eclipse.andmore.swt/feature.xml new file mode 100644 index 00000000..498e1b52 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/feature.xml @@ -0,0 +1,40 @@ + + + + + Android UI Automator Viewer + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/pom.xml b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml new file mode 100644 index 00000000..f2b437b8 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.swt.feature + eclipse-feature + swt base + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.swt/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project new file mode 100644 index 00000000..19e83123 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.swtmenubar.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties b/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.properties b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml new file mode 100644 index 00000000..c20b80a2 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml @@ -0,0 +1,39 @@ + + + + + Android UI Menubar + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml new file mode 100644 index 00000000..314d1759 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.swtmenubar.feature + eclipse-feature + swtmenubar + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.swtmenubar/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project new file mode 100644 index 00000000..3e780074 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.traceviewuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml new file mode 100644 index 00000000..fbb0e73a --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Traceview library + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml new file mode 100644 index 00000000..fb108a9f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.traceviewuilib.feature + eclipse-feature + traceviewuilib + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project new file mode 100644 index 00000000..e133b00c --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.uiautomatorviewer.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.properties b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml new file mode 100644 index 00000000..105107eb --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Automator Viewer + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml new file mode 100644 index 00000000..02b62a9d --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.uiautomatorviewer.feature + eclipse-feature + uiautomatorviewer + + + ../../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath new file mode 100644 index 00000000..6042c676 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore b/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore new file mode 100644 index 00000000..bb9ea140 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore @@ -0,0 +1,3 @@ +/target/ +/bin/ +/libs/ diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.project b/andmore-swt/org.eclipse.andmore.ddmuilib/.project new file mode 100644 index 00000000..b8fbe579 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.ddmuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..662c7dfc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 +encoding/test-src=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..529ef073 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +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.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..ebd3e520 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF @@ -0,0 +1,32 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.ddmuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.core.runtime;bundle-version="3.12.0" +Export-Package: com.android.ddmuilib, + com.android.ddmuilib.actions, + com.android.ddmuilib.annotation, + com.android.ddmuilib.console, + com.android.ddmuilib.explorer, + com.android.ddmuilib.handler, + com.android.ddmuilib.heap, + com.android.ddmuilib.location, + com.android.ddmuilib.log.event, + com.android.ddmuilib.logcat, + com.android.ddmuilib.net, + com.android.ddmuilib.screenrecord, + com.android.ddmuilib.vmtrace +Bundle-ClassPath: ., + libs/chart_swt-1.0.13.jar, + libs/jcommon-1.0.24.jar, + libs/jfreechart-1.0.19.jar, + libs/jfreechart-swt-1.0.jar +Import-Package: org.eclipse.ui.plugin diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE b/andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/README b/andmore-swt/org.eclipse.andmore.ddmuilib/README new file mode 100644 index 00000000..fdfa66e7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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//swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar files +available at prebuild//swt. + +You also need a user library called ANDROID_JFREECHART containing the jar files +available at prebuild/common/jfreechart. diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle b/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle new file mode 100644 index 00000000..6063a7f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle @@ -0,0 +1,15 @@ +group = 'com.android.tools.ddms' +archivesBaseName = 'ddmuilib' + +dependencies { + compile project(':base:ddmlib') + compile 'jfree:jfreechart:1.0.9' + compile 'jfree:jfreechart-swt:1.0.9' + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties b/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties new file mode 100644 index 00000000..f4f7f312 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties @@ -0,0 +1,7 @@ +source.. = src/,\ + src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + libs/,\ + plugin.properties diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml b/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml new file mode 100644 index 00000000..8f694139 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties new file mode 100644 index 00000000..30cbe713 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.ddmuilib +Bundle-Name = Dalvik Debug Monitor Service Library +Bundle-Vendor=Eclipse Andmore Project +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml new file mode 100644 index 00000000..31b0fb70 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml @@ -0,0 +1,157 @@ + + + 4.0.0 + + org.eclipse.andmore.ddmuilib + eclipse-plugin + ddmuilib + + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.jfree + jfreechart + 1.0.19 + + + org.jfree + jfreechart-swt + 1.0 + + + org.jfree + chart_swt + 1.0.13 + + + org.jfree + jcommon + 1.0.24 + + + + + + junit + junit + 4.12 + test + + + + + ${basedir}/test-src + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + test + test + + + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compiletests + test-compile + + testCompile + + + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.build.sourceEncoding} + true + false + -Xlint:none + -Xlint:none + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.0.2 + + + copy + initialize + + copy + + + + + org.jfree + jfreechart + 1.0.19 + jar + + + org.jfree + jfreechart-swt + 1.0 + jar + + + org.jfree + chart_swt + 1.0.13 + jar + + + org.jfree + jcommon + 1.0.24 + jar + + + ${project.basedir}/libs + false + false + true + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java new file mode 100644 index 00000000..9b1bb624 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 00000000..39accfa9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * Represents an addr2line process to get filename/method information from a + * memory address.
+ * Each process can only handle one library, which should be provided when + * creating a new process.
+ *
+ * The processes take some time to load as they need to parse the library files. + * For this reason, processes cannot be manually started. Instead the class + * keeps an internal list of processes and one asks for a process for a specific + * library, using getProcess(String library).

+ * Internally, the processes are started in pipe mode to be able to query them + * with multiple addresses. + */ +public class Addr2Line { + private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS"; + + private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT = + "Unable to locate library %s on disk. Addresses mapping to this library " + + "will not be resolved. In order to fix this, set the the library search path " + + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + "."; + + /** + * Loaded processes list. This is also used as a locking object for any + * methods dealing with starting/stopping/creating processes/querying for + * method. + */ + private static final HashMap sProcessCache = + new HashMap(); + + /** + * byte array representing a carriage return. Used to push addresses in the + * process pipes. + */ + private static final byte[] sCrLf = { + '\n' + }; + + /** Path to the library */ + private NativeLibraryMapInfo mLibrary; + + /** addr2line command to execute */ + private String mAddr2LineCmd; + + /** the command line process */ + private Process mProcess; + + /** buffer to read the result of the command line process from */ + private BufferedReader mResultReader; + + /** + * output stream to provide new addresses to decode to the command line + * process + */ + private BufferedOutputStream mAddressWriter; + + private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER; + static { + String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR); + if (symbols == null) { + DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory(); + } else { + DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols; + } + } + + private static List mLibrarySearchPaths = new ArrayList(); + + /** + * Set the search path where libraries should be found. + * @param path search path to use, can be a colon separated list of paths if multiple folders + * should be searched + */ + public static void setSearchPath(String path) { + mLibrarySearchPaths.clear(); + mLibrarySearchPaths.addAll(Arrays.asList(path.split(":"))); + } + + /** + * Returns the instance of an Addr2Line process for the specified library + * and abi. + *
The library should be in a format that makes
+ * $ANDROID_PRODUCT_OUT + "/symbols" + library a valid file. + * + * @param library the library in which to look for addresses. + * @param abi indicates which underlying addr2line command to use. + * @return a new Addr2Line object representing a started process, ready to + * be queried for addresses. If any error happened when launching a + * new process, null will be returned. + */ + public static Addr2Line getProcess(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { + String libName = library.getLibraryName(); + + // synchronize around the hashmap object + if (libName != null) { + synchronized (sProcessCache) { + // look for an existing process + Addr2Line process = sProcessCache.get(libName); + + // if we don't find one, we create it + if (process == null) { + process = new Addr2Line(library, abi); + + // then we start it + boolean status = process.start(); + + if (status) { + // if starting the process worked, then we add it to the + // list. + sProcessCache.put(libName, process); + } else { + // otherwise we just drop the object, to return null + process = null; + } + } + // return the process + return process; + } + } + return null; + } + + /** + * Construct the object with a library name and abi. The library should be present + * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user + * provided search path. + * + * @param library the library in which to look for address. + * @param abi indicates which underlying addr2line command to use. + */ + private Addr2Line(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { + mLibrary = library; + + // Set the addr2line command based on the abi. + if (abi == null || abi.startsWith("32")) { + Log.d("ddm-Addr2Line", "Using 32 bit addr2line command"); + mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE"); + if (mAddr2LineCmd == null) { + mAddr2LineCmd = DdmUiPreferences.getAddr2Line(); + } + } else { + Log.d("ddm-Addr2Line", "Using 64 bit addr2line command"); + mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE64"); + if (mAddr2LineCmd == null) { + mAddr2LineCmd = DdmUiPreferences.getAddr2Line64(); + } + } + } + + /** + * Search for the library in the library search path and obtain the full path to where it + * is found. + * @return fully resolved path to the library if found in search path, null otherwise + */ + private String getLibraryPath(String library) { + // first check the symbols folder + String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library; + if (new File(path).exists()) { + return path; + } + + for (String p : mLibrarySearchPaths) { + // try appending the full path on device + String fullPath = p + "/" + library; + if (new File(fullPath).exists()) { + return fullPath; + } + + // try appending basename(library) + fullPath = p + "/" + new File(library).getName(); + if (new File(fullPath).exists()) { + return fullPath; + } + } + + return null; + } + + /** + * Starts the command line process. + * + * @return true if the process was started, false if it failed to start, or + * if there was any other errors. + */ + private boolean start() { + // because this is only called from getProcess() we know we don't need + // to synchronize this code. + + // build the command line + String[] command = new String[5]; + command[0] = mAddr2LineCmd; + command[1] = "-C"; + command[2] = "-f"; + command[3] = "-e"; + + String fullPath = getLibraryPath(mLibrary.getLibraryName()); + if (fullPath == null) { + String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName()); + Log.e("ddm-Addr2Line", msg); + return false; + } + + command[4] = fullPath; + + try { + // attempt to start the process + mProcess = Runtime.getRuntime().exec(command); + + if (mProcess != null) { + // get the result reader + InputStreamReader is = new InputStreamReader(mProcess + .getInputStream()); + mResultReader = new BufferedReader(is); + + // get the outstream to write the addresses + mAddressWriter = new BufferedOutputStream(mProcess + .getOutputStream()); + + // check our streams are here + if (mResultReader == null || mAddressWriter == null) { + // not here? stop the process and return false; + mProcess.destroy(); + mProcess = null; + return false; + } + + // return a success + return true; + } + + } catch (IOException e) { + // log the error + String msg = String.format( + "Error while trying to start %1$s process for library %2$s", + mAddr2LineCmd, mLibrary); + Log.e("ddm-Addr2Line", msg); + + // drop the process just in case + if (mProcess != null) { + mProcess.destroy(); + mProcess = null; + } + } + + // we can be here either cause the allocation of mProcess failed, or we + // caught an exception + return false; + } + + /** + * Stops the command line process. + */ + public void stop() { + synchronized (sProcessCache) { + if (mProcess != null) { + // remove the process from the list + sProcessCache.remove(mLibrary); + + // then stops the process + mProcess.destroy(); + + // set the reference to null. + // this allows to make sure another thread calling getAddress() + // will not query a stopped thread + mProcess = null; + } + } + } + + /** + * Stops all current running processes. + */ + public static void stopAll() { + // because of concurrent access (and our use of HashMap.values()), we + // can't rely on the synchronized inside stop(). We need to put one + // around the whole loop. + synchronized (sProcessCache) { + // just a basic loop on all the values in the hashmap and call to + // stop(); + Collection col = sProcessCache.values(); + for (Addr2Line a2l : col) { + a2l.stop(); + } + } + } + + /** + * Looks up an address and returns method name, source file name, and line + * number. + * + * @param addr the address to look up + * @return a BacktraceInfo object containing the method/filename/linenumber + * or null if the process we stopped before the query could be + * processed, or if an IO exception happened. + */ + public NativeStackCallInfo getAddress(long addr) { + long offset = addr - mLibrary.getStartAddress(); + + // even though we don't access the hashmap object, we need to + // synchronized on it to prevent + // another thread from stopping the process we're going to query. + synchronized (sProcessCache) { + // check the process is still alive/allocated + if (mProcess != null) { + // prepare to the write the address to the output buffer. + + // first, conversion to a string containing the hex value. + String tmp = Long.toString(offset, 16); + + try { + // write the address to the buffer + mAddressWriter.write(tmp.getBytes()); + + // add CR-LF + mAddressWriter.write(sCrLf); + + // flush it all. + mAddressWriter.flush(); + + // read the result. We need to read 2 lines + String method = mResultReader.readLine(); + String source = mResultReader.readLine(); + + // make the backtrace object and return it + if (method != null && source != null) { + return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source); + } + } catch (IOException e) { + // log the error + Log.e("ddms", + "Error while trying to get information for addr: " + + tmp + " in library: " + mLibrary); + // we'll return null later + } + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 00000000..04e09d46 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,661 @@ +/* + * 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.andmore.base.resources.ImageFactory; +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.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; + private ImageFactory mImageFactory; + + /** + * 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 + } + } + + public AllocationPanel(ImageFactory imageFactory) + { + super(); + mImageFactory = imageFactory; + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + // get some images + mSortUpImg = mImageFactory.getImageByName("sort_up.png"); + mSortDownImg = mImageFactory.getImageByName("sort_down.png"); + + // 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; + case ALLOCATION_SITE : + break; + case NUMBER : + break; + default : + break; + } + + mSortColumn.setImage(mSorter.isDescending() ? mSortDownImg : mSortUpImg); + + mAllocationViewer = new TableViewer(mAllocationTable); + mAllocationViewer.setContentProvider(new AllocationContentProvider()); + mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); + + mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + AllocationInfo selectedAlloc = getAllocationSelection(event.getSelection()); + updateAllocationStackTrace(selectedAlloc); + } + }); + + // the separating sash + final Sash sash = new Sash(mAllocationBase, SWT.HORIZONTAL); + Color darkGray = parent.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY); + sash.setBackground(darkGray); + + // the UI below the sash + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mAllocationBase, PREFS_STACK_COLUMN, store); + + // now setup the sash. + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + topParent.setLayoutData(data); + + final FormData sashData = new FormData(); + if (store != null && store.contains(PREFS_ALLOC_SASH)) { + sashData.top = new FormAttachment(0, store.getInt(PREFS_ALLOC_SASH)); + } else { + sashData.top = new FormAttachment(50,0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mStackTraceTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = mAllocationBase.getClientArea(); + int bottom = panelRect.height - sashRect.height - 100; + e.y = Math.max(Math.min(e.y, bottom), 100); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + store.setValue(PREFS_ALLOC_SASH, e.y); + mAllocationBase.layout(); + } + } + }); + + return mAllocationBase; + } + + @Override + public void dispose() { + mSortUpImg.dispose(); + mSortDownImg.dispose(); + super.dispose(); + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mAllocationTable.setFocus(); + } + + /** + * Sent when an existing client information changed. + *

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

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

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

+ * the resulting map is {@link #mHeapMap}. + * @param heapData the collection of {@link HeapSegment} that forms the heap data. + */ + private void doSerializeHeapData(Collection heapData) { + mHeapMap = new TreeMap>(); + + Iterator iterator; + ByteArrayOutputStream out; + + out = new ByteArrayOutputStream(4 * 1024); + + iterator = heapData.iterator(); + while (iterator.hasNext()) { + HeapSegment hs = iterator.next(); + + HeapSegmentElement e = null; + while (true) { + int v; + + e = hs.getNextElement(null); + if (e == null) { + break; + } + + if (e.getSolidity() == HeapSegmentElement.SOLIDITY_FREE) { + v = 1; + } else { + v = e.getKind() + 2; + } + + // put the element in the map + ArrayList elementList = mHeapMap.get(v); + if (elementList == null) { + elementList = new ArrayList(); + mHeapMap.put(v, elementList); + } + elementList.add(e); + + + int len = e.getLength() / 8; + while (len > 0) { + out.write(v); + --len; + } + } + } + mProcessedHeapData = out.toByteArray(); + + // sort the segment element in the heap info. + Collection> elementLists = mHeapMap.values(); + for (ArrayList elementList : elementLists) { + Collections.sort(elementList); + } + } + + /** + * Creates a linear image of the heap data. + * @param pixData + * @param h + * @param palette + * @return + */ + protected ImageData createLinearHeapImage(byte[] pixData, int h, PaletteData palette) { + int w = pixData.length / h; + if (pixData.length % h != 0) { + w++; + } + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, palette); + + int x = 0; + int y = 0; + for (byte b : pixData) { + if (b >= 0) { + id.setPixel(x, y, b); + } + + y++; + if (y >= h) { + y = 0; + x++; + } + } + + return id; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 00000000..39ea342d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 00000000..fe519606 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import org.eclipse.jface.preference.IPreferenceStore; + +/** + * Preference entry point for ddmuilib. Allows the lib to access a preference + * store (org.eclipse.jface.preference.IPreferenceStore) defined by the + * application that includes the lib. + */ +public final class DdmUiPreferences { + + public static final int DEFAULT_THREAD_REFRESH_INTERVAL = 4; // seconds + + private static int sThreadRefreshInterval = DEFAULT_THREAD_REFRESH_INTERVAL; + + private static IPreferenceStore mStore; + + private static String sSymbolLocation =""; //$NON-NLS-1$ + private static String sAddr2LineLocation =""; //$NON-NLS-1$ + private static String sAddr2LineLocation64 =""; //$NON-NLS-1$ + private static String sTraceviewLocation =""; //$NON-NLS-1$ + + public static void setStore(IPreferenceStore store) { + mStore = store; + } + + public static IPreferenceStore getStore() { + return mStore; + } + + public static int getThreadRefreshInterval() { + return sThreadRefreshInterval; + } + + public static void setThreadRefreshInterval(int port) { + sThreadRefreshInterval = port; + } + + public static String getSymbolDirectory() { + return sSymbolLocation; + } + + public static void setSymbolsLocation(String location) { + sSymbolLocation = location; + } + + public static String getAddr2Line() { + return sAddr2LineLocation; + } + + public static void setAddr2LineLocation(String location) { + sAddr2LineLocation = location; + } + + public static String getAddr2Line64() { + return sAddr2LineLocation64; + } + + public static void setAddr2LineLocation64(String location) { + sAddr2LineLocation64 = location; + } + + public static String getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 00000000..71137146 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,864 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.annotations.NonNull; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.ClientData.DebuggerStatus; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmuilib.vmtrace.VmTraceOptionsDialog; +import com.google.common.base.Throwables; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * A display of both the devices and their clients. + */ +public final class DevicePanel extends Panel implements IDebugBridgeChangeListener, + IDeviceChangeListener, IClientChangeListener { + + private final static String PREFS_COL_NAME_SERIAL = "devicePanel.Col0"; //$NON-NLS-1$ + private final static String PREFS_COL_PID_STATE = "devicePanel.Col1"; //$NON-NLS-1$ + private final static String PREFS_COL_PORT_BUILD = "devicePanel.Col4"; //$NON-NLS-1$ + + private final static int DEVICE_COL_SERIAL = 0; + private final static int DEVICE_COL_STATE = 1; + // col 2, 3 not used. + private final static int DEVICE_COL_BUILD = 4; + + private final static int CLIENT_COL_NAME = 0; + private final static int CLIENT_COL_PID = 1; + private final static int CLIENT_COL_THREAD = 2; + private final static int CLIENT_COL_HEAP = 3; + private final static int CLIENT_COL_PORT = 4; + + public final static int ICON_WIDTH = 16; + public final static String ICON_THREAD = "thread.png"; //$NON-NLS-1$ + public final static String ICON_HEAP = "heap.png"; //$NON-NLS-1$ + public final static String ICON_HALT = "halt.png"; //$NON-NLS-1$ + public final static String ICON_GC = "gc.png"; //$NON-NLS-1$ + public final static String ICON_HPROF = "hprof.png"; //$NON-NLS-1$ + public final static String ICON_TRACING_START = "tracing_start.png"; //$NON-NLS-1$ + public final static String ICON_TRACING_STOP = "tracing_stop.png"; //$NON-NLS-1$ + + private IDevice mCurrentDevice; + private Client mCurrentClient; + + private Tree mTree; + private TreeViewer mTreeViewer; + + private Image mDeviceImage; + private Image mEmulatorImage; + + private Image mThreadImage; + private Image mHeapImage; + private Image mWaitingImage; + private Image mDebuggerImage; + private Image mDebugErrorImage; + private ImageFactory mImageFactory; + + private final ArrayList mListeners = new ArrayList(); + + private final ArrayList mDevicesToExpand = new ArrayList(); + + private boolean mAdvancedPortSupport; + + /** + * A Content provider for the {@link TreeViewer}. + *

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

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

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

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

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

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + @Override + public void clientChanged(final Client client, final int changeMask) { + exec(new Runnable() { + @Override + public void run() { + if (mTree.isDisposed() == false) { + // refresh the client + mTreeViewer.refresh(client); + + if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == + Client.CHANGE_DEBUGGER_STATUS && + client.getClientData().getDebuggerConnectionStatus() == + DebuggerStatus.WAITING) { + // make sure the device is expanded. Normally the setSelection below + // will auto expand, but the children of device may not already exist + // at this time. Forcing an expand will make the TreeViewer create them. + IDevice device = client.getDevice(); + if (mTreeViewer.getExpandedState(device) == false) { + mTreeViewer.setExpandedState(device, true); + } + + // create and set the selection + TreePath treePath = new TreePath(new Object[] { device, client}); + TreeSelection treeSelection = new TreeSelection(treePath); + mTreeViewer.setSelection(treeSelection); + + if (mAdvancedPortSupport) { + client.setAsSelectedClient(); + } + + // notify the listener of a possible selection change. + notifyListeners(device, client); + } + } else { + // tree is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDebugBridgeChangeListener(DevicePanel.this); + AndroidDebugBridge.removeDeviceChangeListener(DevicePanel.this); + AndroidDebugBridge.removeClientChangeListener(DevicePanel.this); + } + } + }); + } + + private void loadImages(Display display) { + if (mDeviceImage == null) { + String imageName = "device.png"; //$NON-NLS-1$ + mDeviceImage = mImageFactory.getImageByName(imageName); + if (mDeviceImage == null) { + mDeviceImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + } + if (mEmulatorImage == null) { + String imageName = "emulator.png"; //$NON-NLS-1$ + mEmulatorImage = mImageFactory.getImageByName(imageName); + if (mEmulatorImage == null) { + mEmulatorImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + } + if (mThreadImage == null) { + String imageName = ICON_THREAD; + mThreadImage = mImageFactory.getImageByName(imageName); + if (mThreadImage == null) { + mThreadImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + } + if (mHeapImage == null) { + String heapImageName = ICON_HEAP; + mHeapImage = mImageFactory.getImageByName(heapImageName); + if (mHeapImage == null) { + mHeapImage = loadImage(heapImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + } + if (mWaitingImage == null) { + String waitImageName = "debug-wait.png"; //$NON-NLS-1$ + mWaitingImage = mImageFactory.getImageByName(waitImageName); + if (mWaitingImage == null) { + mWaitingImage = loadImage(waitImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + } + if (mDebuggerImage == null) { + String debugImageName = "debug-attach.png"; //$NON-NLS-1$ + mDebuggerImage = mImageFactory.getImageByName(debugImageName); + if (mDebuggerImage == null) { + mDebuggerImage = loadImage(debugImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_GREEN)); + } + } + if (mDebugErrorImage == null) { + String deviceImageName = "device.png"; //$NON-NLS-1$ + mDebugErrorImage = mImageFactory.getImageByName(deviceImageName); + if (mDebugErrorImage == null) { + mDebugErrorImage = loadImage(deviceImageName, display, + ICON_WIDTH, ICON_WIDTH, + 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) { + } + } + } + } + + private Image loadImage(String imageName, Display display, int width, int height, Color color) { + return mImageFactory.getImageByName(imageName, new ReplacementImageFactory(display, width, height, color)); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 00000000..c10ff3fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1462 @@ +/* + * 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.andmore.base.resources.ImageFactory; +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; + private ImageFactory mImageFactory; + + + public EmulatorControlPanel(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + /** + * 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); + + mPlayImage = mImageFactory.getImageByName("play.png"); //$NON-NLS-1$ + mPauseImage = mImageFactory.getImageByName("pause.png"); //$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; + mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxBackwardButton.setImage(mImageFactory.getImageByName("backward.png")); //$NON-NLS-1$ + mGpxBackwardButton.setSelection(false); + mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxForwardButton.setImage(mImageFactory.getImageByName("forward.png")); //$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; + + mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlBackwardButton.setImage(mImageFactory.getImageByName("backward.png")); //$NON-NLS-1$ + mKmlBackwardButton.setSelection(false); + mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlForwardButton.setImage(mImageFactory.getImageByName("forward.png")); //$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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java new file mode 100644 index 00000000..3f2d7c31 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 00000000..dcbbc7f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1311 @@ +/* + * 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.chart.swt.ChartComposite; +import org.jfree.experimental.swt.SWTUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Base class for our information panels. + */ +public final class HeapPanel extends BaseHeapPanel { + private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$ + private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$ + + /* args to setUpdateStatus() */ + private static final int NOT_SELECTED = 0; + private static final int NOT_ENABLED = 1; + private static final int ENABLED = 2; + + /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need + * Native+1 at least. We also need 2 more entries for free area and expansion area. */ + private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1; + private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES]; + private static final PaletteData mMapPalette = createPalette(); + + private static final boolean DISPLAY_HEAP_BITMAP = false; + private static final boolean DISPLAY_HILBERT_BITMAP = false; + + private static final int PLACEHOLDER_HILBERT_SIZE = 200; + private static final int PLACEHOLDER_LINEAR_V_SIZE = 100; + private static final int PLACEHOLDER_LINEAR_H_SIZE = 300; + + private static final int[] ZOOMS = {100, 50, 25}; + + private static final NumberFormat sByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance(); + private static final NumberFormat sCountFormatter = NumberFormat.getInstance(); + + static { + sByteFormatter.setMinimumFractionDigits(0); + sByteFormatter.setMaximumFractionDigits(1); + sLargeByteFormatter.setMinimumFractionDigits(3); + sLargeByteFormatter.setMaximumFractionDigits(3); + + sCountFormatter.setGroupingUsed(true); + } + + private Display mDisplay; + + private Composite mTop; // real top + private Label mUpdateStatus; + private Table mHeapSummary; + private Combo mDisplayMode; + + //private ScrolledComposite mScrolledComposite; + + private Composite mDisplayBase; // base of the displays. + private StackLayout mDisplayStack; + + private Composite mStatisticsBase; + private Table mStatisticsTable; + private JFreeChart mChart; + private ChartComposite mChartComposite; + private Button mGcButton; + private DefaultCategoryDataset mAllocCountDataSet; + + private Composite mLinearBase; + private Label mLinearHeapImage; + + private Composite mHilbertBase; + private Label mHilbertHeapImage; + private Group mLegend; + private Combo mZoom; + + /** Image used for the hilbert display. Since we recreate a new image every time, we + * keep this one around to dispose it. */ + private Image mHilbertImage; + private Image mLinearImage; + private Composite[] mLayout; + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + /** + * Sent when an existing client information changed. + *

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + @Override + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE || + (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) { + try { + mTop.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + clientSelected(); + } + }); + } catch (SWTException e) { + // display is disposed (app is quitting most likely), we do nothing. + } + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mTop.isDisposed()) + return; + + Client client = getCurrentClient(); + + Log.d("ddms", "HeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + + if (client.isHeapUpdateEnabled()) { + mGcButton.setEnabled(true); + mDisplayMode.setEnabled(true); + setUpdateStatus(ENABLED); + } else { + setUpdateStatus(NOT_ENABLED); + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + } + + fillSummaryTable(cd); + + int mode = mDisplayMode.getSelectionIndex(); + if (mode == 0) { + fillDetailedTable(client, false /* forceRedraw */); + } else { + if (DISPLAY_HEAP_BITMAP) { + renderHeapData(cd, mode - 1, false /* forceRedraw */); + } + } + } else { + mGcButton.setEnabled(false); + mDisplayMode.setEnabled(false); + fillSummaryTable(null); + fillDetailedTable(null, true); + setUpdateStatus(NOT_SELECTED); + } + + // sizes of things change frequently, so redo layout + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + GridLayout gl; + + mTop = new Composite(parent, SWT.NONE); + mTop.setLayout(new GridLayout(1, false)); + mTop.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mUpdateStatus = new Label(mTop, SWT.NONE); + setUpdateStatus(NOT_SELECTED); + + Composite summarySection = new Composite(mTop, SWT.NONE); + summarySection.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mHeapSummary = createSummaryTable(summarySection); + mGcButton = new Button(summarySection, SWT.PUSH); + mGcButton.setText("Cause GC"); + mGcButton.setEnabled(false); + mGcButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Client client = getCurrentClient(); + if (client != null) { + client.executeGarbageCollector(); + } + } + }); + + Composite comboSection = new Composite(mTop, SWT.NONE); + gl = new GridLayout(2, false); + gl.marginHeight = gl.marginWidth = 0; + comboSection.setLayout(gl); + + Label displayLabel = new Label(comboSection, SWT.NONE); + displayLabel.setText("Display: "); + + mDisplayMode = new Combo(comboSection, SWT.READ_ONLY); + mDisplayMode.setEnabled(false); + mDisplayMode.add("Stats"); + if (DISPLAY_HEAP_BITMAP) { + mDisplayMode.add("Linear"); + if (DISPLAY_HILBERT_BITMAP) { + mDisplayMode.add("Hilbert"); + } + } + + // the base of the displays. + mDisplayBase = new Composite(mTop, SWT.NONE); + mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDisplayStack = new StackLayout(); + mDisplayBase.setLayout(mDisplayStack); + + // create the statistics display + mStatisticsBase = new Composite(mDisplayBase, SWT.NONE); + //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStatisticsBase.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + mDisplayStack.topControl = mStatisticsBase; + + mStatisticsTable = createDetailedTable(mStatisticsBase); + mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createChart(); + + //create the linear composite + mLinearBase = new Composite(mDisplayBase, SWT.NONE); + //mLinearBase.setLayoutData(new GridData()); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + mLinearBase.setLayout(gl); + + { + mLinearHeapImage = new Label(mLinearBase, SWT.NONE); + mLinearHeapImage.setLayoutData(new GridData()); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(mDisplay, + PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE)); + mLinearHeapImage.setImage(replacementImageFactory.getImage()); + + // 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()); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(mDisplay, + PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE)); + mHilbertHeapImage.setImage(replacementImageFactory.getImage()); + + // create a composite to contain the right part (legend + zoom) + Composite rightSection = new Composite(mHilbertBase, SWT.NONE); + gl = new GridLayout(1, false); + gl.marginHeight = gl.marginWidth = 0; + rightSection.setLayout(gl); + + Composite zoomComposite = new Composite(rightSection, SWT.NONE); + gl = new GridLayout(2, false); + zoomComposite.setLayout(gl); + + Label l = new Label(zoomComposite, SWT.NONE); + l.setText("Zoom:"); + mZoom = new Combo(zoomComposite, SWT.READ_ONLY); + for (int z : ZOOMS) { + mZoom.add(String.format("%1$d%%", z)); //$NON-NLS-1$ + } + + mZoom.select(0); + mZoom.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setLegendText(mZoom.getSelectionIndex()); + Client client = getCurrentClient(); + if (client != null) { + renderHeapData(client.getClientData(), 1, true); + mTop.pack(); + } + } + }); + + createLegend(rightSection); + } + mHilbertBase.pack(); + + mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase }; + mDisplayMode.select(0); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + int index = mDisplayMode.getSelectionIndex(); + Client client = getCurrentClient(); + + if (client != null) { + if (index == 0) { + fillDetailedTable(client, true /* forceRedraw */); + } else { + renderHeapData(client.getClientData(), index-1, true /* forceRedraw */); + } + } + + mDisplayStack.topControl = mLayout[index]; + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + } + }); + + //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT, + // SWT.DEFAULT)); + mDisplayBase.layout(); + //mScrolledComposite.redraw(); + + return mTop; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mHeapSummary.setFocus(); + } + + + private Table createSummaryTable(Composite base) { + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableColumn col; + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("ID"); + col.pack(); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); //$NON-NLS-1$ + col.pack(); + col.setText("Heap Size"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); //$NON-NLS-1$ + col.pack(); + col.setText("Allocated"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.000WW"); //$NON-NLS-1$ + col.pack(); + col.setText("Free"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000.00%"); //$NON-NLS-1$ + col.pack(); + col.setText("% Used"); + + col = new TableColumn(tab, SWT.RIGHT); + col.setText("000,000,000"); //$NON-NLS-1$ + col.pack(); + col.setText("# Objects"); + + // make sure there is always one empty item so that one table row is always displayed. + TableItem item = new TableItem(tab, SWT.NONE); + item.setText(""); + + return tab; + } + + private Table createDetailedTable(Composite base) { + IPreferenceStore store = DdmUiPreferences.getStore(); + + Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION); + tab.setHeaderVisible(true); + tab.setLinesVisible(true); + + TableHelper.createTableColumn(tab, "Type", SWT.LEFT, + "4-byte array (object[], int[], float[])", //$NON-NLS-1$ + PREFS_STATS_COL_TYPE, store); + + TableHelper.createTableColumn(tab, "Count", SWT.RIGHT, + "00,000", //$NON-NLS-1$ + PREFS_STATS_COL_COUNT, store); + + TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SIZE, store); + + TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_SMALLEST, store); + + TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_LARGEST, store); + + TableHelper.createTableColumn(tab, "Median", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_MEDIAN, store); + + TableHelper.createTableColumn(tab, "Average", SWT.RIGHT, + "000.000 WW", //$NON-NLS-1$ + PREFS_STATS_COL_AVERAGE, store); + + tab.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + + Client client = getCurrentClient(); + if (client != null) { + int index = mStatisticsTable.getSelectionIndex(); + TableItem item = mStatisticsTable.getItem(index); + + if (item != null) { + Map> heapMap = + client.getClientData().getVmHeapData().getProcessedHeapMap(); + + ArrayList list = heapMap.get(item.getData()); + if (list != null) { + showChart(list); + } + } + } + + } + }); + + return tab; + } + + /** + * Creates the chart below the statistics table + */ + private void createChart() { + mAllocCountDataSet = new DefaultCategoryDataset(); + mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet, + PlotOrientation.VERTICAL, false, true, false); + + // get the font to make a proper title. We need to convert the swt font, + // into an awt font. + Font f = mStatisticsBase.getFont(); + FontData[] fData = f.getFontData(); + + // event though on Mac OS there could be more than one fontData, we'll only use + // the first one. + FontData firstFontData = fData[0]; + + java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(), + firstFontData, true /* ensureSameSize */); + + mChart.setTitle(new TextTitle("Allocation count per size", awtFont)); + + Plot plot = mChart.getPlot(); + if (plot instanceof CategoryPlot) { + // get the plot + CategoryPlot categoryPlot = (CategoryPlot)plot; + + // set the domain axis to draw labels that are displayed even with many values. + CategoryAxis domainAxis = categoryPlot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90); + + CategoryItemRenderer renderer = categoryPlot.getRenderer(); + renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() { + @Override + public String generateToolTip(CategoryDataset dataset, int row, int column) { + // get the key for the size of the allocation + ByteLong columnKey = (ByteLong)dataset.getColumnKey(column); + String rowKey = (String)dataset.getRowKey(row); + Number value = dataset.getValue(rowKey, columnKey); + + return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey, + columnKey.getValue()); + } + }); + } + mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, // max draw width. We don't want it to zoom, so we put a big number + 3000, // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); // tooltips + + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + } + + private static String prettyByteCount(long bytes) { + double fracBytes = bytes; + String units = " B"; + if (fracBytes < 1024) { + return sByteFormatter.format(fracBytes) + units; + } else { + fracBytes /= 1024; + units = " KB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " MB"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = " GB"; + } + + return sLargeByteFormatter.format(fracBytes) + units; + } + + private static String approximateByteCount(long bytes) { + double fracBytes = bytes; + String units = ""; + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "K"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "M"; + } + if (fracBytes >= 1024) { + fracBytes /= 1024; + units = "G"; + } + + return sByteFormatter.format(fracBytes) + units; + } + + private static String addCommasToNumber(long num) { + return sCountFormatter.format(num); + } + + private static String fractionalPercent(long num, long denom) { + double val = (double)num / (double)denom; + val *= 100; + + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumFractionDigits(2); + nf.setMaximumFractionDigits(2); + return nf.format(val) + "%"; + } + + private void fillSummaryTable(ClientData cd) { + if (mHeapSummary.isDisposed()) { + return; + } + + mHeapSummary.setRedraw(false); + mHeapSummary.removeAll(); + + int numRows = 0; + if (cd != null) { + synchronized (cd) { + Iterator iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + numRows++; + Integer id = iter.next(); + ClientData.HeapInfo info = cd.getVmHeapInfo(id); + if (info == null) { + continue; + } + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(info.sizeInBytes)); + item.setText(2, prettyByteCount(info.bytesAllocated)); + item.setText(3, prettyByteCount(info.sizeInBytes - info.bytesAllocated)); + item.setText(4, fractionalPercent(info.bytesAllocated, info.sizeInBytes)); + item.setText(5, addCommasToNumber(info.objectsAllocated)); + } + } + } + + if (numRows == 0) { + // make sure there is always one empty item so that one table row is always displayed. + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(""); + } + + mHeapSummary.pack(); + mHeapSummary.setRedraw(true); + } + + private void fillDetailedTable(Client client, boolean forceRedraw) { + // first check if the client is invalid or heap updates are not enabled. + if (client == null || client.isHeapUpdateEnabled() == false) { + mStatisticsTable.removeAll(); + showChart(null); + return; + } + + ClientData cd = client.getClientData(); + + Map> heapMap; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + heapMap = cd.getVmHeapData().getProcessedHeapMap(); + } + + // we have new data, lets display it. + + // First, get the current selection, and its key. + int index = mStatisticsTable.getSelectionIndex(); + Integer selectedKey = null; + if (index != -1) { + selectedKey = (Integer)mStatisticsTable.getItem(index).getData(); + } + + // disable redraws and remove all from the table. + mStatisticsTable.setRedraw(false); + mStatisticsTable.removeAll(); + + if (heapMap != null) { + int selectedIndex = -1; + ArrayList selectedList = null; + + // get the keys + Set keys = heapMap.keySet(); + int iter = 0; // use a manual iter int because Set doesn't have an index + // based accessor. + for (Integer key : keys) { + ArrayList list = heapMap.get(key); + + // check if this is the key that is supposed to be selected + if (key.equals(selectedKey)) { + selectedIndex = iter; + selectedList = list; + } + iter++; + + TableItem item = new TableItem(mStatisticsTable, SWT.NONE); + item.setData(key); + + // get the type + item.setText(0, mMapLegend[key]); + + // set the count, smallest, largest + int count = list.size(); + item.setText(1, addCommasToNumber(count)); + + if (count > 0) { + item.setText(3, prettyByteCount(list.get(0).getLength())); + item.setText(4, prettyByteCount(list.get(count-1).getLength())); + + int median = count / 2; + HeapSegmentElement element = list.get(median); + long size = element.getLength(); + item.setText(5, prettyByteCount(size)); + + long totalSize = 0; + for (int i = 0 ; i < count; i++) { + element = list.get(i); + + size = element.getLength(); + totalSize += size; + } + + // set the average and total + item.setText(2, prettyByteCount(totalSize)); + item.setText(6, prettyByteCount(totalSize / count)); + } + } + + mStatisticsTable.setRedraw(true); + + if (selectedIndex != -1) { + mStatisticsTable.setSelection(selectedIndex); + showChart(selectedList); + } else { + showChart(null); + } + } else { + mStatisticsTable.setRedraw(true); + } + } + + private static class ByteLong implements Comparable { + private long mValue; + + private ByteLong(long value) { + mValue = value; + } + + public long getValue() { + return mValue; + } + + @Override + public String toString() { + return approximateByteCount(mValue); + } + + @Override + public int compareTo(ByteLong other) { + if (mValue != other.mValue) { + return mValue < other.mValue ? -1 : 1; + } + return 0; + } + + } + + /** + * Fills the chart with the content of the list of {@link HeapSegmentElement}. + */ + private void showChart(ArrayList list) { + mAllocCountDataSet.clear(); + + if (list != null) { + String rowKey = "Alloc Count"; + + long currentSize = -1; + int currentCount = 0; + for (HeapSegmentElement element : list) { + if (element.getLength() != currentSize) { + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + + currentSize = element.getLength(); + currentCount = 1; + } else { + currentCount++; + } + } + + // add the last item + if (currentSize != -1) { + ByteLong columnKey = new ByteLong(currentSize); + mAllocCountDataSet.addValue(currentCount, rowKey, columnKey); + } + } + } + + /* + * Add a color legend to the specified table. + */ + private void createLegend(Composite parent) { + mLegend = new Group(parent, SWT.NONE); + mLegend.setText(getLegendText(0)); + + mLegend.setLayout(new GridLayout(2, false)); + + RGB[] colors = mMapPalette.colors; + + for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) { + Image tmpImage = createColorRect(parent.getDisplay(), colors[i]); + + Label l = new Label(mLegend, SWT.NONE); + l.setImage(tmpImage); + + l = new Label(mLegend, SWT.NONE); + l.setText(mMapLegend[i]); + } + } + + private String getLegendText(int level) { + int bytes = 8 * (100 / ZOOMS[level]); + + return String.format("Key (1 pixel = %1$d bytes)", bytes); + } + + private void setLegendText(int level) { + mLegend.setText(getLegendText(level)); + + } + + /* + * Create a nice rectangle in the specified color. + */ + private Image createColorRect(Display display, RGB color) { + int width = 32; + int height = 16; + + Image img = new Image(display, width, height); + GC gc = new GC(img); + gc.setBackground(new Color(display, color)); + gc.fillRectangle(0, 0, width, height); + gc.dispose(); + return img; + } + + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap updates"); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + + "NOT ENABLED for this client"); + break; + case ENABLED: + mUpdateStatus.setText("Heap updates will happen after " + + "every GC for this client"); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + + /** + * Return the closest power of two greater than or equal to value. + * + * @param value the return value will be >= value + * @return a power of two >= value. If value > 2^31, 2^31 is returned. + */ +//xxx use Integer.highestOneBit() or numberOfLeadingZeros(). + private int nextPow2(int value) { + for (int i = 31; i >= 0; --i) { + if ((value & (1<>> 2) & 1) << 1 | + ((i >>> 4) & 1) << 2 | + ((i >>> 6) & 1) << 3 | + ((i >>> 8) & 1) << 4 | + ((i >>> 10) & 1) << 5 | + ((i >>> 12) & 1) << 6 | + ((i >>> 14) & 1) << 7 | + ((i >>> 16) & 1) << 8 | + ((i >>> 18) & 1) << 9 | + ((i >>> 20) & 1) << 10 | + ((i >>> 22) & 1) << 11 | + ((i >>> 24) & 1) << 12 | + ((i >>> 26) & 1) << 13 | + ((i >>> 28) & 1) << 14 | + ((i >>> 30) & 1) << 15; + int y = ((i >>> 1) & 1) << 0 | + ((i >>> 3) & 1) << 1 | + ((i >>> 5) & 1) << 2 | + ((i >>> 7) & 1) << 3 | + ((i >>> 9) & 1) << 4 | + ((i >>> 11) & 1) << 5 | + ((i >>> 13) & 1) << 6 | + ((i >>> 15) & 1) << 7 | + ((i >>> 17) & 1) << 8 | + ((i >>> 19) & 1) << 9 | + ((i >>> 21) & 1) << 10 | + ((i >>> 23) & 1) << 11 | + ((i >>> 25) & 1) << 12 | + ((i >>> 27) & 1) << 13 | + ((i >>> 29) & 1) << 14 | + ((i >>> 31) & 1) << 15; + try { + id.setPixel(x, y, pixData[i]); + if (x > maxX) { + maxX = x; + } + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: i " + i + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } + return maxX; + } + + private final static int HILBERT_DIR_N = 0; + private final static int HILBERT_DIR_S = 1; + private final static int HILBERT_DIR_E = 2; + private final static int HILBERT_DIR_W = 3; + + private void hilbertWalk(ImageData id, InputStream pixData, + int order, int x, int y, int dir) + throws IOException { + if (x >= id.width || y >= id.height) { + return; + } else if (order == 0) { + try { + int p = pixData.read(); + if (p >= 0) { + // flip along x=y axis; assume width == height + id.setPixel(y, x, p); + + /* Skanky; use an otherwise-unused ImageData field + * to keep track of the max x,y used. Note that x and y are inverted. + */ + if (y > id.x) { + id.x = y; + } + if (x > id.y) { + id.y = x; + } + } +//xxx just give up; don't bother walking the rest of the image + } catch (IllegalArgumentException ex) { + System.out.println("bad pixels: order " + order + + ", dir " + dir + + ", w " + id.width + + ", h " + id.height + + ", x " + x + + ", y " + y); + throw ex; + } + } else { + order--; + int delta = 1 << order; + int nextX = x + delta; + int nextY = y + delta; + + switch (dir) { + case HILBERT_DIR_E: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_S); + break; + case HILBERT_DIR_N: + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_E); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_W); + break; + case HILBERT_DIR_S: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_E); + break; + case HILBERT_DIR_W: + hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S); + hilbertWalk(id, pixData, order, nextX, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, y, HILBERT_DIR_W); + hilbertWalk(id, pixData, order, x, nextY, HILBERT_DIR_N); + break; + default: + throw new RuntimeException("Unexpected Hilbert direction " + + dir); + } + } + } + + private Point hilbertOrderData(ImageData id, byte pixData[]) { + + int order = 0; + for (int n = 1; n < id.width; n *= 2) { + order++; + } + /* Skanky; use an otherwise-unused ImageData field + * to keep track of maxX. + */ + Point p = new Point(0,0); + int oldIdX = id.x; + int oldIdY = id.y; + id.x = id.y = 0; + try { + hilbertWalk(id, new ByteArrayInputStream(pixData), + order, 0, 0, HILBERT_DIR_E); + p.x = id.x; + p.y = id.y; + } catch (IOException ex) { + System.err.println("Exception during hilbertWalk()"); + p.x = id.height; + p.y = id.width; + } + id.x = oldIdX; + id.y = oldIdY; + return p; + } + + private ImageData createHilbertHeapImage(byte pixData[]) { + int w, h; + + // Pick an image size that the largest of heaps will fit into. + w = (int)Math.sqrt(((16 * 1024 * 1024)/8)); + + // Space-filling curves require a power-of-2 width. + w = nextPow2(w); + h = w; + + // Create the heap image. + ImageData id = new ImageData(w, h, 8, mMapPalette); + + // Copy the data into the image + //int maxX = zOrderData(id, pixData); + Point maxP = hilbertOrderData(id, pixData); + + // update the max size to make it a round number once the zoom is applied + int factor = 100 / ZOOMS[mZoom.getSelectionIndex()]; + if (factor != 1) { + int tmp = maxP.x % factor; + if (tmp != 0) { + maxP.x += factor - tmp; + } + + tmp = maxP.y % factor; + if (tmp != 0) { + maxP.y += factor - tmp; + } + } + + if (maxP.y < id.height) { + // Crop the image down to the interesting part. + id = new ImageData(id.width, maxP.y, id.depth, id.palette, + id.scanlinePad, id.data); + } + + if (maxP.x < id.width) { + // crop the image again. A bit trickier this time. + ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette); + + int[] buffer = new int[maxP.x]; + for (int l = 0 ; l < id.height; l++) { + id.getPixels(0, l, maxP.x, buffer, 0); + croppedId.setPixels(0, l, maxP.x, buffer, 0); + } + + id = croppedId; + } + + // apply the zoom + if (factor != 1) { + id = id.scaledTo(id.width / factor, id.height / factor); + } + + return id; + } + + /** + * Convert the raw heap data to an image. We know we're running in + * the UI thread, so we can issue graphics commands directly. + * + * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html + * + * @param cd The client data + * @param mode The display mode. 0 = linear, 1 = hilbert. + * @param forceRedraw + */ + private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) { + Image image; + + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + } + + if (pixData != null) { + ImageData id; + if (mode == 1) { + id = createHilbertHeapImage(pixData); + } else { + id = createLinearHeapImage(pixData, 200, mMapPalette); + } + + image = new Image(mDisplay, id); + } else { + // Render a placeholder image. + int width, height; + if (mode == 1) { + width = height = PLACEHOLDER_HILBERT_SIZE; + } else { + width = PLACEHOLDER_LINEAR_H_SIZE; + height = PLACEHOLDER_LINEAR_V_SIZE; + } + image = new Image(mDisplay, width, height); + GC gc = new GC(image); + gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(0, 0, width-1, height-1); + gc.dispose(); + gc = null; + } + + // set the new image + + if (mode == 1) { + if (mHilbertImage != null) { + mHilbertImage.dispose(); + } + + mHilbertImage = image; + mHilbertHeapImage.setImage(mHilbertImage); + mHilbertHeapImage.pack(true); + mHilbertBase.layout(); + mHilbertBase.pack(true); + } else { + if (mLinearImage != null) { + mLinearImage.dispose(); + } + + mLinearImage = image; + mLinearHeapImage.setImage(mLinearImage); + mLinearHeapImage.pack(true); + mLinearBase.layout(); + mLinearBase.pack(true); + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mHeapSummary); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java new file mode 100644 index 00000000..a5dfef44 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 00000000..9ad4c0e3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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.
+ * 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 00000000..49520947 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +/** + * Display client info in a two-column format. + */ +public class InfoPanel extends TablePanel { + private Table mTable; + private TableColumn mCol2; + + private static final String mLabels[] = { + "DDM-aware?", + "App description:", + "VM version:", + "Process ID:", + "Supports Profiling Control:", + "Supports HPROF Control:", + "ABI Flavor:", + "JVM Flags:", + }; + private static final int ENT_DDM_AWARE = 0; + private static final int ENT_APP_DESCR = 1; + private static final int ENT_VM_VERSION = 2; + private static final int ENT_PROCESS_ID = 3; + private static final int ENT_SUPPORTS_PROFILING = 4; + private static final int ENT_SUPPORTS_HPROF = 5; + private static final int ENT_ABI_FLAVOR = 6; + private static final int ENT_JVM_FLAGS = 7; + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + mTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mTable.setHeaderVisible(false); + mTable.setLinesVisible(false); + + TableColumn col1 = new TableColumn(mTable, SWT.RIGHT); + col1.setText("name"); + mCol2 = new TableColumn(mTable, SWT.LEFT); + mCol2.setText("PlaceHolderContentForWidth"); + + TableItem item; + for (int i = 0; i < mLabels.length; i++) { + item = new TableItem(mTable, SWT.NONE); + item.setText(0, mLabels[i]); + item.setText(1, "-"); + } + + col1.pack(); + mCol2.pack(); + + return mTable; + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTable.setFocus(); + } + + + /** + * Sent when an existing client information changed. + *

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

+ * This is sent from a non UI thread. + * @param client the updated client. + * @param changeMask the bit mask describing the changed properties. It can contain + * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} + * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, + * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, + * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} + * + * @see IClientChangeListener#clientChanged(Client, int) + */ + @Override + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) { + if (mBase.isDisposed()) + return; + + mBase.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + clientSelected(); + } + }); + } + } + } + + /** + * Sent when a new device is selected. The new device can be accessed + * with {@link #getCurrentDevice()}. + */ + @Override + public void deviceSelected() { + // pass + } + + /** + * Sent when a new client is selected. The new client can be accessed + * with {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + if (mBase.isDisposed()) + return; + + Client client = getCurrentClient(); + + mDisplayModeCombo.setEnabled(false); + emptyTables(); + + Log.d("ddms", "NativeHeapPanel: changed " + client); + + if (client != null) { + ClientData cd = client.getClientData(); + mClientData = cd; + + // if (cd.getShowHeapUpdates()) + setUpdateStatus(ENABLED); + // else + // setUpdateStatus(NOT_ENABLED); + + initAllocationDisplay(); + + //renderBitmap(cd); + } else { + mClientData = null; + setUpdateStatus(NOT_SELECTED); + } + + mBase.pack(); + } + + /** + * Update the UI with the newly compute stack calls, unless the UI switched + * to a different client. + * + * @param cd the ClientData for which the stack call are being computed. + * @param count the current count of allocations for which the stack calls + * have been computed. + */ + @WorkerThread + public void updateAllocationStackCalls(ClientData cd, int count) { + // we have to check that the panel still shows the same clientdata than + // the thread is computing for. + if (cd == mClientData) { + + int total = mAllocations.size(); + + if (count == total) { + // we're done: do something + mDisplayModeCombo.setEnabled(true); + mSaveButton.setEnabled(true); + + mStackCallThread = null; + } else { + // work in progress, update the progress bar. +// mUiThread.setStatusLine("Computing stack call: " + count +// + "/" + total); + } + + // FIXME: attempt to only update when needed. + // Because the number of pages is not related to mAllocations.size() anymore + // due to pre-zygote/post-zygote display, update all the time. + // At some point we should remove the pages anyway, since it's getting computed + // really fast now. +// if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count +// || (count == total && mCurrentPage == mPageCount - 1)) { + try { + // get the current selection of the allocation + int index = mAllocationTable.getSelectionIndex(); + NativeAllocationInfo info = null; + + if (index != -1) { + info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData(); + } + + // empty the table + emptyTables(); + + // fill it again + fillAllocationTable(); + + // reselect + mAllocationTable.setSelection(index); + + // display detail table if needed + if (info != null) { + fillDetailTable(info); + } + } catch (SWTException e) { + if (mAllocationTable.isDisposed()) { + // looks like the table is disposed. Let's ignore it. + } else { + throw e; + } + } + + } else { + // old client still running. doesn't really matter. + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mAllocationTable); + addTableToFocusListener(mLibraryTable); + addTableToFocusListener(mLibraryAllocationTable); + addTableToFocusListener(mDetailTable); + } + + protected void onAllocDisplayChange() { + mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex(); + + // create the new list + updateAllocDisplayList(); + + updateTotalMemoryDisplay(); + + // reset the ui. + mCurrentPage = 0; + updatePageUI(); + switchDisplayMode(); + } + + private void updateAllocDisplayList() { + mTotalSize = 0; + mDisplayedAllocations.clear(); + for (NativeAllocationInfo info : mAllocations) { + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL || + (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) { + mDisplayedAllocations.add(info); + mTotalSize += info.getSize() * info.getAllocationCount(); + } else { + // skip this item + continue; + } + } + + int count = mDisplayedAllocations.size(); + + mPageCount = count / DISPLAY_PER_PAGE; + + // need to add a page for the rest of the div + if ((count % DISPLAY_PER_PAGE) > 0) { + mPageCount++; + } + } + + private void updateTotalMemoryDisplay() { + switch (mAllocDisplayMode) { + case ALLOC_DISPLAY_ALL: + mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_PRE_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + case ALLOC_DISPLAY_POST_ZYGOTE: + mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes", + sFormatter.format(mTotalSize))); + break; + } + } + + + private void switchDisplayMode() { + switch (mDisplayModeCombo.getSelectionIndex()) { + case 0: {// allocations + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mAllocationModeTop; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillAllocationTable(); + } + break; + case 1: {// libraries + mTopStackLayout.topControl = mTableModeControl; + mAllocationStackLayout.topControl = mLibraryModeTopControl; + mAllocationStackComposite.layout(); + mTopStackComposite.layout(); + emptyTables(); + fillLibraryTable(); + } + break; + } + } + + private void initAllocationDisplay() { + if (mStackCallThread != null) { + mStackCallThread.quit(); + } + + mAllocations.clear(); + mAllocations.addAll(mClientData.getNativeAllocationList()); + + updateAllocDisplayList(); + + // if we have a previous clientdata and it matches the current one. we + // do a diff between the new list and the old one. + if (mBackUpClientData != null && mBackUpClientData == mClientData) { + + ArrayList add = new ArrayList(); + + // we go through the list of NativeAllocationInfo in the new list and check if + // there's one with the same exact data (size, allocation, count and + // stackcall addresses) in the old list. + // if we don't find any, we add it to the "add" list + for (NativeAllocationInfo mi : mAllocations) { + boolean found = false; + for (NativeAllocationInfo old_mi : mBackUpAllocations) { + if (mi.equals(old_mi)) { + found = true; + break; + } + } + if (found == false) { + add.add(mi); + } + } + + // put the result in mAllocations + mAllocations.clear(); + mAllocations.addAll(add); + + // display the difference in memory usage. This is computed + // calculating the memory usage of the objects in mAllocations. + int count = 0; + for (NativeAllocationInfo allocInfo : mAllocations) { + count += allocInfo.getSize() * allocInfo.getAllocationCount(); + } + + mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes", + sFormatter.format(count))); + } + else { + // display the full memory usage + updateTotalMemoryDisplay(); + //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0); + } + mTotalMemoryLabel.pack(); + + // update the page ui + mDisplayModeCombo.select(0); + + mLibraryAllocations.clear(); + + // reset to first page + mCurrentPage = 0; + + // update the label + updatePageUI(); + + // now fill the allocation Table with the current page + switchDisplayMode(); + + // start the thread to compute the stack calls + if (mAllocations.size() > 0) { + mStackCallThread = new StackCallThread(mClientData); + mStackCallThread.start(); + } + } + + private void updatePageUI() { + + // set the label and pack to update the layout, otherwise + // the label will be cut off if the new size is bigger + if (mPageCount == 0) { + mPageLabel.setText("0 of 0 allocations."); + } else { + StringBuffer buffer = new StringBuffer(); + // get our starting index + int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1; + // end index, taking into account the last page can be half full + int count = mDisplayedAllocations.size(); + int end = Math.min(start + DISPLAY_PER_PAGE - 1, count); + buffer.append(sFormatter.format(start)); + buffer.append(" - "); + buffer.append(sFormatter.format(end)); + buffer.append(" of "); + buffer.append(sFormatter.format(count)); + buffer.append(" allocations."); + mPageLabel.setText(buffer.toString()); + } + + // handle the button enabled state. + mPagePreviousButton.setEnabled(mCurrentPage > 0); + // reminder: mCurrentPage starts at 0. + mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1); + + mPageLabel.pack(); + mPageUIComposite.pack(); + + } + + private void fillAllocationTable() { + // get the count + int count = mDisplayedAllocations.size(); + + // get our starting index + int start = mCurrentPage * DISPLAY_PER_PAGE; + + // loop for DISPLAY_PER_PAGE or till we reach count + int end = start + DISPLAY_PER_PAGE; + + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = mDisplayedAllocations.get(i); + + TableItem item = null; + + if (mAllocDisplayMode == ALLOC_DISPLAY_ALL) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, (info.isZygoteChild() ? "Z " : "") + + sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) { + item = new TableItem(mAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + } else { + // skip this item + continue; + } + + item.setData(info); + + NativeStackCallInfo bti = info.getRelevantStackCallInfo(); + if (bti != null) { + String lib = bti.getLibraryName(); + String method = bti.getMethodName(); + String source = bti.getSourceFile(); + if (lib != null) + item.setText(3, lib); + if (method != null) + item.setText(4, method); + if (source != null) + item.setText(5, source); + } + } + } + + private void fillLibraryTable() { + // fill the library table + sortAllocationsPerLibrary(); + + for (LibraryAllocations liballoc : mLibraryAllocations) { + if (liballoc != null) { + TableItem item = new TableItem(mLibraryTable, SWT.NONE); + String lib = liballoc.getLibrary(); + item.setText(0, lib != null ? lib : ""); + item.setText(1, sFormatter.format(liballoc.getSize())); + item.setText(2, sFormatter.format(liballoc.getCount())); + } + } + } + + private void fillLibraryAllocationTable() { + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations liballoc = mLibraryAllocations.get(index); + // start a thread that will fill table 10 at a time to keep the ui + // responsive, but first we kill the previous one if there was one + if (mFillTableThread != null) { + mFillTableThread.quit(); + } + mFillTableThread = new FillTableThread(liballoc, + liballoc.getAllocationSize()); + mFillTableThread.start(); + } + } + + public void updateLibraryAllocationTable(LibraryAllocations liballoc, + int start, int end) { + try { + if (mLibraryTable.isDisposed() == false) { + int index = mLibraryTable.getSelectionIndex(); + if (index != -1) { + LibraryAllocations newliballoc = mLibraryAllocations.get( + index); + if (newliballoc == liballoc) { + int count = liballoc.getAllocationSize(); + for (int i = start; i < end && i < count; i++) { + NativeAllocationInfo info = liballoc.getAllocation(i); + + TableItem item = new TableItem( + mLibraryAllocationTable, SWT.NONE); + item.setText(0, sFormatter.format( + info.getSize() * info.getAllocationCount())); + item.setText(1, sFormatter.format(info.getAllocationCount())); + item.setText(2, sFormatter.format(info.getSize())); + + NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + item.setText(3, stackCallInfo.getMethodName()); + } + } + } else { + // we should quit the thread + if (mFillTableThread != null) { + mFillTableThread.quit(); + mFillTableThread = null; + } + } + } + } + } catch (SWTException e) { + Log.e("ddms", "error when updating the library allocation table"); + } + } + + private void fillDetailTable(final NativeAllocationInfo mi) { + mDetailTable.removeAll(); + mDetailTable.setRedraw(false); + + try { + // populate the detail Table with the back trace + List addresses = mi.getStackCallAddresses(); + List resolvedStackCall = mi.getResolvedStackCall(); + + if (resolvedStackCall == null) { + return; + } + + for (int i = 0 ; i < resolvedStackCall.size(); i++) { + if (addresses.get(i) == null || addresses.get(i).longValue() == 0) { + continue; + } + + long addr = addresses.get(i).longValue(); + NativeStackCallInfo source = resolvedStackCall.get(i); + + TableItem item = new TableItem(mDetailTable, SWT.NONE); + item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$ + + String libraryName = source.getLibraryName(); + String methodName = source.getMethodName(); + String sourceFile = source.getSourceFile(); + int lineNumber = source.getLineNumber(); + + if (libraryName != null) + item.setText(1, libraryName); + if (methodName != null) + item.setText(2, methodName); + if (sourceFile != null) + item.setText(3, sourceFile); + if (lineNumber != -1) + item.setText(4, Integer.toString(lineNumber)); + } + } finally { + mDetailTable.setRedraw(true); + } + } + + /* + * Are updates enabled? + */ + private void setUpdateStatus(int status) { + switch (status) { + case NOT_SELECTED: + mUpdateStatus.setText("Select a client to see heap info"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case NOT_ENABLED: + mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client"); + mAllocDisplayCombo.setEnabled(false); + mFullUpdateButton.setEnabled(false); + //mDiffUpdateButton.setEnabled(false); + break; + case ENABLED: + mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data"); + mAllocDisplayCombo.setEnabled(true); + mFullUpdateButton.setEnabled(true); + //mDiffUpdateButton.setEnabled(true); + break; + default: + throw new RuntimeException(); + } + + mUpdateStatus.pack(); + } + + /** + * Create the Table display. This includes a "detail" Table in the bottom + * half and 2 modes in the top half: allocation Table and + * library+allocations Tables. + * + * @param base the top parent to create the display into + */ + private void createTableDisplay(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // top level composite for mode 1 & 2 + mTableModeControl = new Composite(base, SWT.NONE); + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + mTableModeControl.setLayout(gl); + mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE); + mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTotalMemoryLabel.setText("Total Memory: 0 Bytes"); + + // the top half of these modes is dynamic + + final Composite sash_composite = new Composite(mTableModeControl, + SWT.NONE); + sash_composite.setLayout(new FormLayout()); + sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // create the stacked composite + mAllocationStackComposite = new Composite(sash_composite, SWT.NONE); + mAllocationStackLayout = new StackLayout(); + mAllocationStackComposite.setLayout(mAllocationStackLayout); + mAllocationStackComposite.setLayoutData(new GridData( + GridData.FILL_BOTH)); + + // create the top half for mode 1 + createAllocationTopHalf(mAllocationStackComposite); + + // create the top half for mode 2 + createLibraryTopHalf(mAllocationStackComposite); + + final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL); + + // bottom half of these modes is the same: detail table + createDetailTable(sash_composite); + + // init value for stack + mAllocationStackLayout.topControl = mAllocationModeTop; + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(mTotalMemoryLabel, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mAllocationStackComposite.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) { + sashData.top = new FormAttachment(0, + prefs.getInt(PREFS_ALLOCATION_SASH)); + } else { + sashData.top = new FormAttachment(50, 0); // 50% across + } + sashData.left = new FormAttachment(0, 0); + sashData.right = new FormAttachment(100, 0); + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(sash, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + mDetailTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = sash_composite.getClientArea(); + int bottom = panelRect.height - sashRect.height - minPanelWidth; + e.y = Math.max(Math.min(e.y, bottom), minPanelWidth); + if (e.y != sashRect.y) { + sashData.top = new FormAttachment(0, e.y); + prefs.setValue(PREFS_ALLOCATION_SASH, e.y); + sash_composite.layout(); + } + } + }); + } + + private void createDetailTable(Composite base) { + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mDetailTable.setHeaderVisible(true); + mDetailTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT, + "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT, + "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$ + } + + private void createAllocationTopHalf(Composite b) { + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + Composite base = new Composite(b, SWT.NONE); + mAllocationModeTop = base; + GridLayout gl = new GridLayout(1, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.verticalSpacing = 0; + base.setLayout(gl); + base.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // horizontal layout for memory total and pages UI + mPageUIComposite = new Composite(base, SWT.NONE); + mPageUIComposite.setLayoutData(new GridData( + GridData.HORIZONTAL_ALIGN_BEGINNING)); + gl = new GridLayout(3, false); + gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0; + gl.horizontalSpacing = 0; + mPageUIComposite.setLayout(gl); + + // Page UI + mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE); + mPagePreviousButton.setText("<"); + mPagePreviousButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage--; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageNextButton = new Button(mPageUIComposite, SWT.NONE); + mPageNextButton.setText(">"); + mPageNextButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mCurrentPage++; + updatePageUI(); + emptyTables(); + fillAllocationTable(); + } + }); + + mPageLabel = new Label(mPageUIComposite, SWT.NONE); + mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + updatePageUI(); + + mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION); + mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT, + "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT, + "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT, + "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT, + "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$ + + mAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection index + int index = mAllocationTable.getSelectionIndex(); + if (index >= 0 && index < mAllocationTable.getItemCount()) { + TableItem item = mAllocationTable.getItem(index); + if (item != null && item.getData() instanceof NativeAllocationInfo) { + fillDetailTable((NativeAllocationInfo)item.getData()); + } + } + } + }); + } + + private void createLibraryTopHalf(Composite base) { + final int minPanelWidth = 60; + + final IPreferenceStore prefs = DdmUiPreferences.getStore(); + + // create a composite that'll contain 2 tables horizontally + final Composite top = new Composite(base, SWT.NONE); + mLibraryModeTopControl = top; + top.setLayout(new FormLayout()); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // first table: library + mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryTable.setHeaderVisible(true); + mLibraryTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT, + "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT, + "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT, + "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$ + + mLibraryTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + fillLibraryAllocationTable(); + } + }); + + final Sash sash = new Sash(top, SWT.VERTICAL); + + // 2nd table: allocation per library + mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION); + mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mLibraryAllocationTable.setHeaderVisible(true); + mLibraryAllocationTable.setLinesVisible(true); + + TableHelper.createTableColumn(mLibraryAllocationTable, "Total", + SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Count", + SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Size", + SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$ + TableHelper.createTableColumn(mLibraryAllocationTable, "Method", + SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$ + + mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the index of the selection in the library table + int index1 = mLibraryTable.getSelectionIndex(); + // get the index in the library allocation table + int index2 = mLibraryAllocationTable.getSelectionIndex(); + // get the MallocInfo object + if (index1 != -1 && index2 != -1) { + LibraryAllocations liballoc = mLibraryAllocations.get(index1); + NativeAllocationInfo info = liballoc.getAllocation(index2); + fillDetailTable(info); + } + } + }); + + // form layout data + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(sash, 0); + mLibraryTable.setLayoutData(data); + + final FormData sashData = new FormData(); + if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) { + sashData.left = new FormAttachment(0, + prefs.getInt(PREFS_LIBRARY_SASH)); + } else { + sashData.left = new FormAttachment(50, 0); + } + sashData.bottom = new FormAttachment(100, 0); + sashData.top = new FormAttachment(0, 0); // 50% across + sash.setLayoutData(sashData); + + data = new FormData(); + data.top = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.left = new FormAttachment(sash, 0); + data.right = new FormAttachment(100, 0); + mLibraryAllocationTable.setLayoutData(data); + + // allow resizes, but cap at minPanelWidth + sash.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = top.getClientArea(); + int right = panelRect.width - sashRect.width - minPanelWidth; + e.x = Math.max(Math.min(e.x, right), minPanelWidth); + if (e.x != sashRect.x) { + sashData.left = new FormAttachment(0, e.x); + prefs.setValue(PREFS_LIBRARY_SASH, e.y); + top.layout(); + } + } + }); + } + + private void emptyTables() { + mAllocationTable.removeAll(); + mLibraryTable.removeAll(); + mLibraryAllocationTable.removeAll(); + mDetailTable.removeAll(); + } + + private void sortAllocationsPerLibrary() { + if (mClientData != null) { + mLibraryAllocations.clear(); + + // create a hash map of LibraryAllocations to access aggregate + // objects already created + HashMap libcache = + new HashMap(); + + // get the allocation count + int count = mDisplayedAllocations.size(); + for (int i = 0; i < count; i++) { + NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i); + + NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo(); + if (stackCallInfo != null) { + String libraryName = stackCallInfo.getLibraryName(); + LibraryAllocations liballoc = libcache.get(libraryName); + if (liballoc == null) { + // didn't find a library allocation object already + // created so we create one + liballoc = new LibraryAllocations(libraryName); + // add it to the cache + libcache.put(libraryName, liballoc); + // add it to the list + mLibraryAllocations.add(liballoc); + } + // add the MallocInfo object to it. + liballoc.addAllocation(allocInfo); + } + } + // now that the list is created, we need to compute the size and + // sort it by size. This will also sort the MallocInfo objects + // inside each LibraryAllocation objects. + for (LibraryAllocations liballoc : mLibraryAllocations) { + liballoc.computeAllocationSizeAndCount(); + } + + // now we sort it + Collections.sort(mLibraryAllocations, + new Comparator() { + @Override + public int compare(LibraryAllocations o1, + LibraryAllocations o2) { + return o2.getSize() - o1.getSize(); + } + }); + } + } + + private void renderBitmap(ClientData cd) { + byte[] pixData; + + // Atomically get and clear the heap data. + synchronized (cd) { + if (serializeHeapData(cd.getVmHeapData()) == false) { + // no change, we return. + return; + } + + pixData = getSerializedData(); + + ImageData id = createLinearHeapImage(pixData, 200, mMapPalette); + Image image = new Image(mBase.getDisplay(), id); + mImage.setImage(image); + mImage.pack(true); + } + } + + /* + * Create color palette for map. Set up titles for legend. + */ + private static PaletteData createPalette() { + RGB colors[] = new RGB[NUM_PALETTE_ENTRIES]; + colors[0] + = new RGB(192, 192, 192); // non-heap pixels are gray + mMapLegend[0] + = "(heap expansion area)"; + + colors[1] + = new RGB(0, 0, 0); // free chunks are black + mMapLegend[1] + = "free"; + + colors[HeapSegmentElement.KIND_OBJECT + 2] + = new RGB(0, 0, 255); // objects are blue + mMapLegend[HeapSegmentElement.KIND_OBJECT + 2] + = "data object"; + + colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = new RGB(0, 255, 0); // class objects are green + mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2] + = "class object"; + + colors[HeapSegmentElement.KIND_ARRAY_1 + 2] + = new RGB(255, 0, 0); // byte/bool arrays are red + mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2] + = "1-byte array (byte[], boolean[])"; + + colors[HeapSegmentElement.KIND_ARRAY_2 + 2] + = new RGB(255, 128, 0); // short/char arrays are orange + mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2] + = "2-byte array (short[], char[])"; + + colors[HeapSegmentElement.KIND_ARRAY_4 + 2] + = new RGB(255, 255, 0); // obj/int/float arrays are yellow + mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2] + = "4-byte array (object[], int[], float[])"; + + colors[HeapSegmentElement.KIND_ARRAY_8 + 2] + = new RGB(255, 128, 128); // long/double arrays are pink + mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2] + = "8-byte array (long[], double[])"; + + colors[HeapSegmentElement.KIND_UNKNOWN + 2] + = new RGB(255, 0, 255); // unknown objects are cyan + mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2] + = "unknown object"; + + colors[HeapSegmentElement.KIND_NATIVE + 2] + = new RGB(64, 64, 64); // native objects are dark gray + mMapLegend[HeapSegmentElement.KIND_NATIVE + 2] + = "non-Java object"; + + return new PaletteData(colors); + } + + private void saveAllocations(String fileName) { + try { + PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); + + for (NativeAllocationInfo alloc : mAllocations) { + out.println(alloc.toString()); + } + out.close(); + } catch (IOException e) { + Log.e("Native", e); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Panel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Panel.java new file mode 100644 index 00000000..dadde285 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java new file mode 100644 index 00000000..63c50ca6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.andmore.base.resources.ImageFactory.ReplacementImager; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.widgets.Display; + +public class ReplacementImageFactory implements ReplacementImager { + + private Display display; + private int width; + private int height; + private Color color; + + /** + * @param display Display object + * @param width Width to create replacement Image + * @param height Height to create replacement Image + * @param color Optional color to create replacement Image. If null, Blue + * color will be used. + */ + public ReplacementImageFactory(Display display, int width, int height, Color color) + { + this.display = display; + this.width = width; + this.height = height; + this.color = color != null ? color : display.getSystemColor(SWT.COLOR_BLUE); + } + + @Override + public ImageData create() { + Image img = getImage(); + ImageData imageData = img.getImageData(); + img.dispose(); + return imageData; + } + + public Image getImage() { + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 00000000..8a379c1c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.TimeoutException; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.ImageTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.IOException; +import java.util.Calendar; + + +/** + * Gather a screen shot from the device and save it to a file. + */ +public class ScreenShotDialog extends Dialog { + + private Label mBusyLabel; + private Label mImageLabel; + private Button mSave; + private IDevice mDevice; + private RawImage mRawImage; + private Clipboard mClipboard; + + /** Number of 90 degree rotations applied to the current image */ + private int mRotateCount = 0; + + /** + * Create with default style. + */ + public ScreenShotDialog(Shell parent) { + this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + mClipboard = new Clipboard(parent.getDisplay()); + } + + /** + * Create with app-defined style. + */ + public ScreenShotDialog(Shell parent, int style) { + super(parent, style); + } + + /** + * Prepare and display the dialog. + * @param device The {@link IDevice} from which to get the screenshot. + */ + public void open(IDevice device) { + mDevice = device; + + Shell parent = getParent(); + Shell shell = new Shell(parent, getStyle()); + shell.setText("Device Screen Capture"); + + createContents(shell); + shell.pack(); + shell.open(); + + updateDeviceImage(shell); + + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + } + + /* + * Create the screen capture dialog contents. + */ + private void createContents(final Shell shell) { + GridData data; + + final int colCount = 5; + + shell.setLayout(new GridLayout(colCount, true)); + + // "refresh" button + Button refresh = new Button(shell, SWT.PUSH); + refresh.setText("Refresh"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + refresh.setLayoutData(data); + refresh.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateDeviceImage(shell); + // RawImage only allows us to rotate the image 90 degrees at the time, + // so to preserve the current rotation we must call getRotated() + // the same number of times the user has done it manually. + // TODO: improve the RawImage class. + if (mRawImage != null) { + for (int i = 0; i < mRotateCount; i++) { + mRawImage = mRawImage.getRotated(); + } + updateImageDisplay(shell); + } + } + }); + + // "rotate" button + Button rotate = new Button(shell, SWT.PUSH); + rotate.setText("Rotate"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + rotate.setLayoutData(data); + rotate.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (mRawImage != null) { + mRotateCount = (mRotateCount + 1) % 4; + mRawImage = mRawImage.getRotated(); + updateImageDisplay(shell); + } + } + }); + + // "save" button + mSave = new Button(shell, SWT.PUSH); + mSave.setText("Save"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + mSave.setLayoutData(data); + mSave.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + saveImage(shell); + } + }); + + Button copy = new Button(shell, SWT.PUSH); + copy.setText("Copy"); + copy.setToolTipText("Copy the screenshot to the clipboard"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + copy.setLayoutData(data); + copy.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + copy(); + } + }); + + + // "done" button + Button done = new Button(shell, SWT.PUSH); + done.setText("Done"); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.widthHint = 80; + done.setLayoutData(data); + done.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + shell.close(); + } + }); + + // title/"capturing" label + mBusyLabel = new Label(shell, SWT.NONE); + mBusyLabel.setText("Preparing..."); + data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + data.horizontalSpan = colCount; + mBusyLabel.setLayoutData(data); + + // space for the image + mImageLabel = new Label(shell, SWT.BORDER); + data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER); + data.horizontalSpan = colCount; + mImageLabel.setLayoutData(data); + Display display = shell.getDisplay(); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)); + mImageLabel.setImage(replacementImageFactory.getImage()); + 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(); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); + image = replacementImageFactory.getImage(); + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 00000000..2c7b17f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 00000000..6b96e42b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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

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

This method will set the parent {@link Composite} to use a {@link GridLayout} with + * 2 columns. + * @param parent the parent composite. + * @param prefs_stack_column + * @param store + */ + public Table createPanel(Composite parent, String prefs_stack_column, + IPreferenceStore store) { + + mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mStackTraceTable.setHeaderVisible(false); + mStackTraceTable.setLinesVisible(false); + + TableHelper.createTableColumn( + mStackTraceTable, + "Info", + SWT.LEFT, + "SomeLongClassName.method(android/somepackage/someotherpackage/somefile.java:99999)", //$NON-NLS-1$ + prefs_stack_column, store); + + mStackTraceViewer = new TableViewer(mStackTraceTable); + mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); + mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); + + mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + public void doubleClick(DoubleClickEvent event) { + if (sSourceRevealer != null && mCurrentClient != null) { + // get the selected stack trace element + ISelection selection = mStackTraceViewer.getSelection(); + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof StackTraceElement) { + StackTraceElement traceElement = (StackTraceElement)object; + + if (traceElement.isNativeMethod() == false) { + sSourceRevealer.reveal( + mCurrentClient.getClientData().getClientDescription(), + traceElement.getClassName(), + traceElement.getLineNumber()); + } + } + } + } + } + }); + + return mStackTraceTable; + } + + /** + * Sets the input for the {@link TableViewer}. + * @param input the {@link IStackTraceInfo} that will provide the viewer with the list of + * {@link StackTraceElement} + */ + public void setViewerInput(IStackTraceInfo input) { + mStackTraceViewer.setInput(input); + mStackTraceViewer.refresh(); + } + + /** + * Sets the current client running the stack trace. + * @param currentClient the {@link Client}. + */ + public void setCurrentClient(Client currentClient) { + mCurrentClient = currentClient; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java new file mode 100644 index 00000000..ce3f2045 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java new file mode 100644 index 00000000..4fd13c40 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 00000000..48f9af07 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,931 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib; + +import com.android.annotations.concurrency.GuardedBy; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; +import com.android.ddmlib.NullOutputReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.chart.swt.ChartComposite; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Displays system information graphs obtained from a bugreport file or device. + */ +public class SysinfoPanel extends TablePanel { + + // UI components + private Label mLabel; + private Button mFetchButton; + private Combo mDisplayMode; + + private DefaultPieDataset mDataset; + private DefaultCategoryDataset mBarDataSet; + + private StackLayout mStackLayout; + private Composite mChartComposite; + private Composite mPieChartComposite; + private Composite mStackedBarComposite; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + private String mGfxPackageName; + + private static final Object RECEIVER_LOCK = new Object(); + @GuardedBy("RECEIVER_LOCK") + private ShellOutputReceiver mLastOutputReceiver; + + private static final int MODE_CPU = 0; + private static final int MODE_MEMINFO = 1; + private static final int MODE_GFXINFO = 2; + + // argument to dumpsys; section in the bugreport holding the data + private static final String DUMP_COMMAND[] = { + "dumpsys cpuinfo", + "cat /proc/meminfo ; procrank", + "dumpsys gfxinfo", + }; + + private static final String CAPTIONS[] = { + "CPU load", + "Memory usage", + "Frame Render Time", + }; + + /** Shell property that controls whether graphics profiling is enabled or not. */ + private static final String PROP_GFX_PROFILING = "debug.hwui.profile"; //$NON-NLS-1$ + + /** + * Generates the dataset to display. + * + * @param file The bugreport file to process. + */ + private void generateDataset(File file) { + if (file == null) { + return; + } + try { + BufferedReader br = getBugreportReader(file); + if (mMode == MODE_CPU) { + readCpuDataset(br); + } else if (mMode == MODE_MEMINFO) { + readMeminfoDataset(br); + } else if (mMode == MODE_GFXINFO) { + readGfxInfoDataset(br); + } + br.close(); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + /** + * Sent when a new device is selected. The new device can be accessed with + * {@link #getCurrentDevice()} + */ + @Override + public void deviceSelected() { + if (getCurrentDevice() != null) { + mFetchButton.setEnabled(true); + loadFromDevice(); + } else { + mFetchButton.setEnabled(false); + } + } + + /** + * Sent when a new client is selected. The new client can be accessed with + * {@link #getCurrentClient()}. + */ + @Override + public void clientSelected() { + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mDisplayMode.setFocus(); + } + + /** + * Fetches a new bugreport from the device and updates the display. + * Fetching is asynchronous. See also addOutput, flush, and isCancelled. + */ + private void loadFromDevice() { + clearDataSet(); + + if (mMode == MODE_GFXINFO) { + boolean en = isGfxProfilingEnabled(); + if (!en) { + if (enableGfxProfiling()) { + MessageDialog.openInformation(Display.getCurrent().getActiveShell(), + "DDMS", + "Graphics profiling was enabled on the device.\n" + + "It may be necessary to relaunch your application to see profile information."); + } else { + MessageDialog.openError(Display.getCurrent().getActiveShell(), + "DDMS", + "Unexpected error enabling graphics profiling on device.\n"); + return; + } + } + } + + final String command = getDumpsysCommand(mMode); + if (command == null) { + return; + } + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + String header = null; + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + header = "------ MEMORY INFO ------\n"; + } + + IShellOutputReceiver receiver = initShellOutputBuffer(header); + getCurrentDevice().executeShellCommand(command, receiver); + } catch (IOException e) { + Log.e("DDMS", e); + } catch (TimeoutException e) { + Log.e("DDMS", e); + } catch (AdbCommandRejectedException e) { + Log.e("DDMS", e); + } catch (ShellCommandUnresponsiveException e) { + Log.e("DDMS", e); + } + } + }, "Sysinfo Output Collector"); + t.start(); + } + + private boolean isGfxProfilingEnabled() { + IDevice device = getCurrentDevice(); + if (device == null) { + return false; + } + + String prop; + try { + Future future = device.getSystemProperty(PROP_GFX_PROFILING); + prop = future.get(); + return prop != null ? Boolean.valueOf(prop) : false; + } catch (Exception e) { + return false; + } + } + + private boolean enableGfxProfiling() { + IDevice device = getCurrentDevice(); + if (device == null) { + return false; + } + + try { + device.executeShellCommand("setprop " + PROP_GFX_PROFILING + " true", + new NullOutputReceiver()); + } catch (Exception e) { + return false; + } + + return true; + } + + private String getDumpsysCommand(int mode) { + if (mode == MODE_GFXINFO) { + Client c = getCurrentClient(); + if (c == null) { + return null; + } + + ClientData cd = c.getClientData(); + if (cd == null) { + return null; + } + + mGfxPackageName = cd.getClientDescription(); + if (mGfxPackageName == null) { + return null; + } + + return "dumpsys gfxinfo " + mGfxPackageName; + } else if (mode < DUMP_COMMAND.length) { + return DUMP_COMMAND[mode]; + } + + return null; + } + + /** + * Initializes temporary output file for executeShellCommand(). + * + * @throws IOException on file error + */ + IShellOutputReceiver initShellOutputBuffer(String header) throws IOException { + File f = File.createTempFile("ddmsfile", ".txt"); + f.deleteOnExit(); + + synchronized (RECEIVER_LOCK) { + if (mLastOutputReceiver != null) { + mLastOutputReceiver.cancel(); + } + + mLastOutputReceiver = new ShellOutputReceiver(f, header); + } + return mLastOutputReceiver; + } + + /** + * Create our controls for the UI panel. + */ + @Override + protected Control createControl(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Composite buttons = new Composite(top, SWT.NONE); + buttons.setLayout(new RowLayout()); + + mDisplayMode = new Combo(buttons, SWT.PUSH); + for (String mode : CAPTIONS) { + mDisplayMode.add(mode); + } + mDisplayMode.select(mMode); + mDisplayMode.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mMode = mDisplayMode.getSelectionIndex(); + if (getCurrentDevice() != null) { + loadFromDevice(); + } + } + }); + + mFetchButton = new Button(buttons, SWT.PUSH); + mFetchButton.setText("Update from Device"); + mFetchButton.setEnabled(false); + mFetchButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + loadFromDevice(); + } + }); + + mLabel = new Label(top, SWT.NONE); + mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mChartComposite = new Composite(top, SWT.NONE); + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStackLayout = new StackLayout(); + mChartComposite.setLayout(mStackLayout); + + mPieChartComposite = createPieChartComposite(mChartComposite); + mStackedBarComposite = createStackedBarComposite(mChartComposite); + + mStackLayout.topControl = mPieChartComposite; + + return top; + } + + private Composite createStackedBarComposite(Composite chartComposite) { + mBarDataSet = new DefaultCategoryDataset(); + JFreeChart chart = ChartFactory.createStackedBarChart("Per Frame Rendering Time", + "Frame #", "Time (ms)", mBarDataSet, PlotOrientation.VERTICAL, + true /* legend */, true /* tooltips */, false /* urls */); + + ChartComposite c = newChartComposite(chart, chartComposite); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + return c; + } + + private Composite createPieChartComposite(Composite chartComposite) { + mDataset = new DefaultPieDataset(); + JFreeChart chart = ChartFactory.createPieChart("", mDataset, false + /* legend */, true/* tooltips */, false /* urls */); + + ChartComposite c = newChartComposite(chart, chartComposite); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + return c; + } + + private ChartComposite newChartComposite(JFreeChart chart, Composite parent) { + return new ChartComposite(parent, + SWT.BORDER, chart, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, + 3000, + // max draw width. We don't want it to zoom, so we put a big number + 3000, + // max draw height. We don't want it to zoom, so we put a big number + true, // off-screen buffer + true, // properties + true, // save + true, // print + false, // zoom + true); + } + + @Override + public void clientChanged(final Client client, int changeMask) { + // Don't care + } + + /** + * Helper to open a bugreport and skip to the specified section. + * + * @param file File to open + * @return Reader to bugreport file + * @throws java.io.IOException on file error + */ + private BufferedReader getBugreportReader(File file) throws + IOException { + return new BufferedReader(new FileReader(file)); + } + + /** + * Parse the time string generated by BatteryStats. + * A typical new-format string is "11d 13h 45m 39s 999ms". + * A typical old-format string is "12.3 sec". + * @return time in ms + */ + private static long parseTimeMs(String s) { + long total = 0; + // Matches a single component e.g. "12.3 sec" or "45ms" + Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); + Matcher m = p.matcher(s); + while (m.find()) { + String label = m.group(2); + if ("sec".equals(label)) { + // Backwards compatibility with old time format + total += (long) (Double.parseDouble(m.group(1)) * 1000); + continue; + } + long value = Integer.parseInt(m.group(1)); + if ("d".equals(label)) { + total += value * 24 * 60 * 60 * 1000; + } else if ("h".equals(label)) { + total += value * 60 * 60 * 1000; + } else if ("m".equals(label)) { + total += value * 60 * 1000; + } else if ("s".equals(label)) { + total += value * 1000; + } else if ("ms".equals(label)) { + total += value; + } + } + return total; + } + + public static final class BugReportParser { + public static final class DataValue { + final String name; + final double value; + + public DataValue(String n, double v) { + name = n; + value = v; + } + }; + + /** Components of the time it takes to draw a single frame. */ + public static final class GfxProfileData { + /** draw time (time spent building display lists) in ms */ + final double draw; + + /** process time (time spent by Android's 2D renderer to execute display lists) (ms) */ + final double process; + + /** execute time (time spent to send frame to the compositor) in ms */ + final double execute; + + public GfxProfileData(double draw, double process, double execute) { + this.draw = draw; + this.process = process; + this.execute = execute; + } + } + + public static List parseGfxInfo(BufferedReader br) throws IOException { + Pattern headerPattern = Pattern.compile("\\s+Draw\\s+Process\\s+Execute"); + + String line = null; + while ((line = br.readLine()) != null) { + Matcher m = headerPattern.matcher(line); + if (m.find()) { + break; + } + } + + if (line == null) { + return Collections.emptyList(); + } + + // parse something like: " 0.85 1.10 0.61\n", 3 doubles basically + Pattern dataPattern = + Pattern.compile("(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)"); + + List data = new ArrayList(128); + while ((line = br.readLine()) != null) { + Matcher m = dataPattern.matcher(line); + if (!m.find()) { + break; + } + + double draw = safeParseDouble(m.group(1)); + double process = safeParseDouble(m.group(2)); + double execute = safeParseDouble(m.group(3)); + + data.add(new GfxProfileData(draw, process, execute)); + } + + return data; + } + + /** + * Processes wakelock information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readWakelockDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + + Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); + Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); + double total = 0; + boolean inCurrent = false; + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith("Current Battery Usage Statistics")) { + inCurrent = true; + } else if (inCurrent) { + Matcher m = lockPattern.matcher(line); + if (m.find()) { + double value = parseTimeMs(m.group(2)) / 1000.; + results.add(new DataValue(m.group(1), value)); + total -= value; + } else { + m = totalPattern.matcher(line); + if (m.find()) { + total += parseTimeMs(m.group(1)) / 1000.; + } + } + } + } + if (total > 0) { + results.add(new DataValue("Unlocked", total)); + } + + return results; + } + + /** + * Processes alarm information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readAlarmDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + Pattern pattern = Pattern.compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + Matcher m = pattern.matcher(line); + if (m.find()) { + long count = Long.parseLong(m.group(1)); + String name = m.group(2); + results.add(new DataValue(name, count)); + } + } + + return results; + } + + /** + * Processes cpu load information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readCpuDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + Pattern pattern1 = Pattern.compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); + Pattern pattern2 = Pattern.compile("(\\S+)% (\\S+): (.+)% user . (.+)% kernel"); + + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + line = line.trim(); + + if (line.startsWith("Load:")) { + continue; + } + + String name = ""; + double user = 0, kernel = 0, both = 0; + boolean found = false; + + // try pattern1 + Matcher m = pattern1.matcher(line); + if (m.find()) { + found = true; + name = m.group(1); + both = safeParseLong(m.group(2)); + user = safeParseLong(m.group(3)); + kernel = safeParseLong(m.group(4)); + } + + // try pattern2 + m = pattern2.matcher(line); + if (m.find()) { + found = true; + name = m.group(2); + both = safeParseDouble(m.group(1)); + user = safeParseDouble(m.group(3)); + kernel = safeParseDouble(m.group(4)); + } + + if (!found) { + continue; + } + + if ("TOTAL".equals(name)) { + if (both < 100) { + results.add(new DataValue("Idle", (100 - both))); + } + } else { + // Try to make graphs more useful even with rounding; + // log often has 0% user + 0% kernel = 1% total + // We arbitrarily give extra to kernel + if (user > 0) { + results.add(new DataValue(name + " (user)", user)); + } + if (kernel > 0) { + results.add(new DataValue(name + " (kernel)" , both - user)); + } + if (user == 0 && kernel == 0 && both > 0) { + results.add(new DataValue(name, both)); + } + } + + } + + return results; + } + + private static long safeParseLong(String s) { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return 0; + } + } + + private static double safeParseDouble(String s) { + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Processes meminfo information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readMeminfoDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + Pattern valuePattern = Pattern.compile("(\\d+) kB"); + long total = 0; + long other = 0; + + // Scan meminfo + String line = null; + while ((line = br.readLine()) != null) { + if (line.contains("----")) { + continue; + } + + Matcher m = valuePattern.matcher(line); + if (m.find()) { + long kb = Long.parseLong(m.group(1)); + if (line.startsWith("MemTotal")) { + total = kb; + } else if (line.startsWith("MemFree")) { + results.add(new DataValue("Free", kb)); + total -= kb; + } else if (line.startsWith("Slab")) { + results.add(new DataValue("Slab", kb)); + total -= kb; + } else if (line.startsWith("PageTables")) { + results.add(new DataValue("PageTables", kb)); + total -= kb; + } else if (line.startsWith("Buffers") && kb > 0) { + results.add(new DataValue("Buffers", kb)); + total -= kb; + } else if (line.startsWith("Inactive")) { + results.add(new DataValue("Inactive", kb)); + total -= kb; + } else if (line.startsWith("MemFree")) { + results.add(new DataValue("Free", kb)); + total -= kb; + } + } else { + break; + } + } + + List procRankResults = readProcRankDataset(br, line); + for (DataValue procRank : procRankResults) { + if (procRank.value > 2000) { // only show processes using > 2000K in memory + results.add(procRank); + } else { + other += procRank.value; + } + + total -= procRank.value; + } + + if (other > 0) { + results.add(new DataValue("Other", other)); + } + + // The Pss calculation is not necessarily accurate as accounting memory to + // a process is not accurate. So only if there really is unaccounted for memory do we + // add it to the pie. + if (total > 0) { + results.add(new DataValue("Unknown", total)); + } + + return results; + } + + static List readProcRankDataset(BufferedReader br, String header) + throws IOException { + List results = new ArrayList(); + + if (header == null || !header.contains("PID")) { + return results; + } + + Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults(); + List fields = Lists.newArrayList(PROCRANK_SPLITTER.split(header)); + int pssIndex = fields.indexOf("Pss"); + int cmdIndex = fields.indexOf("cmdline"); + + if (pssIndex == -1 || cmdIndex == -1) { + return results; + } + + String line; + while ((line = br.readLine()) != null) { + // Extract pss field from procrank output + fields = Lists.newArrayList(PROCRANK_SPLITTER.split(line)); + + if (fields.size() < cmdIndex) { + break; + } + + String cmdline = fields.get(cmdIndex).replace("/system/bin/", ""); + String pssInK = fields.get(pssIndex); + if (pssInK.endsWith("K")) { + pssInK = pssInK.substring(0, pssInK.length() - 1); + } + long pss = safeParseLong(pssInK); + results.add(new DataValue(cmdline, pss)); + } + + return results; + } + + /** + * Processes sync information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readSyncDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + + while (true) { + String line = br.readLine(); + if (line == null || line.startsWith("DUMP OF SERVICE")) { + // Done, or moved on to the next service + break; + } + if (line.startsWith(" |") && line.length() > 70) { + String authority = line.substring(3, 18).trim(); + String duration = line.substring(61, 70).trim(); + // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) + String durParts[] = duration.split(":"); + if (durParts.length == 2) { + long dur = Long.parseLong(durParts[0]) * 60 + Long + .parseLong(durParts[1]); + results.add(new DataValue(authority, dur)); + } else if (duration.length() == 3) { + long dur = Long.parseLong(durParts[0]) * 3600 + + Long.parseLong(durParts[1]) * 60 + Long + .parseLong(durParts[2]); + results.add(new DataValue(authority, dur)); + } + } + } + + return results; + } + } + + private void readCpuDataset(BufferedReader br) throws IOException { + updatePieDataSet(BugReportParser.readCpuDataset(br), ""); + } + + private void readMeminfoDataset(BufferedReader br) throws IOException { + updatePieDataSet(BugReportParser.readMeminfoDataset(br), "PSS in kB"); + } + + private void readGfxInfoDataset(BufferedReader br) throws IOException { + updateBarChartDataSet(BugReportParser.parseGfxInfo(br), + mGfxPackageName == null ? "" : mGfxPackageName); + } + + private void clearDataSet() { + mLabel.setText(""); + mDataset.clear(); + mBarDataSet.clear(); + } + + private void updatePieDataSet(final List data, final String label) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mLabel.setText(label); + mStackLayout.topControl = mPieChartComposite; + mChartComposite.layout(); + + for (BugReportParser.DataValue d : data) { + mDataset.setValue(d.name, d.value); + } + } + }); + } + + private void updateBarChartDataSet(final List gfxProfileData, + final String label) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mLabel.setText(label); + mStackLayout.topControl = mStackedBarComposite; + mChartComposite.layout(); + + for (int i = 0; i < gfxProfileData.size(); i++) { + GfxProfileData d = gfxProfileData.get(i); + String frameNumber = Integer.toString(i); + + mBarDataSet.addValue(d.draw, "Draw", frameNumber); + mBarDataSet.addValue(d.process, "Process", frameNumber); + mBarDataSet.addValue(d.execute, "Execute", frameNumber); + } + } + }); + } + + private class ShellOutputReceiver implements IShellOutputReceiver { + private final OutputStream mStream; + private final File mFile; + private AtomicBoolean mCancelled = new AtomicBoolean(); + + public ShellOutputReceiver(File f, String header) { + mFile = f; + try { + mStream = new FileOutputStream(f); + } catch (FileNotFoundException e) { + throw new IllegalArgumentException(e); + } + + if (header != null) { + byte[] data = header.getBytes(); + addOutput(data, 0, data.length); + } + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + try { + mStream.write(data, offset, length); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + @Override + public void flush() { + try { + mStream.close(); + } catch (IOException e) { + Log.e("DDMS", e); + } + + if (!isCancelled()) { + generateDataset(mFile); + } + } + + @Override + public boolean isCancelled() { + return mCancelled.get(); + } + + public void cancel() { + mCancelled.set(true); + } + + public File getDataFile() { + return mFile; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java new file mode 100644 index 00000000..5449a348 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + * @return The TableColumn object that was created + */ + public static TableColumn createTableColumn(Table parent, String header, + int style, String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TableColumn col = new TableColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TableColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + + return col; + } + + /** + * Create a TreeColumn with the specified parameters. If a + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param sample_text A sample text to figure out column width if preference + * value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + String sample_text, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setText(sample_text); + col.pack(); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, col.getWidth()); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } + + /** + * Create a TreeColumn with the specified parameters. If a + * PreferenceStore object and a preference entry name String + * object are provided then the column will listen to change in its width + * and update the preference store accordingly. + * + * @param parent The Table parent object + * @param header The header string + * @param style The column style + * @param width the width of the column if the preference value is missing + * @param pref_name The preference entry name for column width + * @param prefs The preference store + */ + public static void createTreeColumn(Tree parent, String header, int style, + int width, final String pref_name, + final IPreferenceStore prefs) { + + // create the column + TreeColumn col = new TreeColumn(parent, style); + + // if there is no pref store or the entry is missing, we use the sample + // text and pack the column. + // Otherwise we just read the width from the prefs and apply it. + if (prefs == null || prefs.contains(pref_name) == false) { + col.setWidth(width); + + // init the prefs store with the current value + if (prefs != null) { + prefs.setValue(pref_name, width); + } + } else { + col.setWidth(prefs.getInt(pref_name)); + } + + // set the header + col.setText(header); + + // if there is a pref store and a pref entry name, then we setup a + // listener to catch column resize to put store the new width value. + if (prefs != null && pref_name != null) { + col.addControlListener(new ControlListener() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + public void controlResized(ControlEvent e) { + // get the new width + int w = ((TreeColumn)e.widget).getWidth(); + + // store in pref store + prefs.setValue(pref_name, w); + } + }); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java new file mode 100644 index 00000000..ce6867fa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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.
+ * Default implementation does nothing. + */ + protected void setTableFocusListener() { + + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus. + * + * @param table the Table object. + * @param colStart + * @param colEnd + */ + protected final void addTableToFocusListener(final Table table, + final int colStart, final int colEnd) { + // create the activator for this table + final IFocusedTableActivator activator = new IFocusedTableActivator() { + @Override + public void copy(Clipboard clipboard) { + int[] selection = table.getSelectionIndices(); + + // we need to sort the items to be sure. + Arrays.sort(selection); + + // all lines must be concatenated. + StringBuilder sb = new StringBuilder(); + + // loop on the selection and output the file. + for (int i : selection) { + TableItem item = table.getItem(i); + for (int c = colStart ; c <= colEnd ; c++) { + sb.append(item.getText(c)); + sb.append('\t'); + } + sb.append('\n'); + } + + // now add that to the clipboard if the string has content + String data = sb.toString(); + if (data != null && data.length() > 0) { + clipboard.setContents( + new Object[] { data }, + new Transfer[] { TextTransfer.getInstance() }); + } + } + + @Override + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + @Override + public void focusLost(FocusEvent e) { + mGlobalListener.focusLost(activator); + } + }); + } + + /** + * Sets up a Table object to notify the global Table Focus listener when it + * gets or loses the focus.
+ * When the copy method is invoked, all columns are put in the clipboard, separated + * by tabs + * + * @param table the Table object. + */ + protected final void addTableToFocusListener(final Table table) { + addTableToFocusListener(table, 0, table.getColumnCount()-1); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 00000000..ea94602a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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

+ * This must be called from the UI thread. + */ + private void updateThreadStackCall() { + Client client = getCurrentClient(); + if (client != null) { + // get the current selection in the ThreadTable + ThreadInfo selectedThread = getThreadSelection(null); + + if (selectedThread != null) { + updateThreadStackTrace(selectedThread); + } else { + updateThreadStackTrace(null); + } + } + } + + /** + * updates the stackcall of the specified thread. If null the UI is emptied + * of current data. + * @param thread + */ + private void updateThreadStackTrace(ThreadInfo thread) { + mStackTracePanel.setViewerInput(thread); + + if (thread != null) { + mRefreshStackTraceButton.setEnabled(true); + long stackcallTime = thread.getStackCallTime(); + if (stackcallTime != 0) { + String label = new Date(stackcallTime).toString(); + mStackTraceTimeLabel.setText(label); + } else { + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } else { + mRefreshStackTraceButton.setEnabled(true); + mStackTraceTimeLabel.setText(""); //$NON-NLS-1$ + } + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mThreadTable); + addTableToFocusListener(mStackTraceTable); + } + + /** + * Initiate recurring events. We use a shorter "initialWait" so we do the + * first execution sooner. We don't do it immediately because we want to + * give the clients a chance to get set up. + */ + private void startRecurringThreadUpdate() { + mRecurringThreadUpdateRunning = true; + int initialWait = 1000; + + mDisplay.timerExec(initialWait, new Runnable() { + @Override + public void run() { + synchronized (mLock) { + // lets check we still want updates. + if (mMustStopRecurringThreadUpdate == false) { + Client client = getCurrentClient(); + if (client != null) { + client.requestThreadUpdate(); + + mDisplay.timerExec( + DdmUiPreferences.getThreadRefreshInterval() * 1000, this); + } else { + // we don't have a Client, which means the runnable is not + // going to be called through the timer. We reset the running flag. + mRecurringThreadUpdateRunning = false; + } + } else { + // else actually stops (don't call the timerExec) and reset the flags. + mRecurringThreadUpdateRunning = false; + mMustStopRecurringThreadUpdate = false; + } + } + } + }); + } + + /** + * Returns the current thread selection or null if none is found. + * If a {@link ISelection} object is specified, the first {@link ThreadInfo} from this selection + * is returned, otherwise, the ISelection returned by + * {@link TableViewer#getSelection()} is used. + * @param selection the {@link ISelection} to use, or null + */ + private ThreadInfo getThreadSelection(ISelection selection) { + if (selection == null) { + selection = mThreadViewer.getSelection(); + } + + if (selection instanceof IStructuredSelection) { + IStructuredSelection structuredSelection = (IStructuredSelection)selection; + Object object = structuredSelection.getFirstElement(); + if (object instanceof ThreadInfo) { + return (ThreadInfo)object; + } + } + + return null; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 00000000..7d65c48e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 true to enable, and + * false to disable + */ + public void setEnabled(boolean enabled); + + /** + * Sets the checked status of this action. + * @param checked the new checked status + */ + public void setChecked(boolean checked); + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered. + */ + public void setRunnable(Runnable runnable); +} + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 00000000..6747d032 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 true to enable, and + * false to disable + * @see ICommonAction#setChecked(boolean) + */ + @Override + public void setChecked(boolean checked) { + item.setSelection(checked); + } + + /** + * Sets the enabled state of this action. + * @param enabled true to enable, and + * false to disable + * @see ICommonAction#setEnabled(boolean) + */ + @Override + public void setEnabled(boolean enabled) { + item.setEnabled(enabled); + } + + /** + * Sets the {@link Runnable} that will be executed when the action is triggered (through + * {@link SelectionListener#widgetSelected(SelectionEvent)} on the wrapped {@link ToolItem}). + * @see ICommonAction#setRunnable(Runnable) + */ + @Override + public void setRunnable(final Runnable runnable) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + runnable.run(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 00000000..32b61b22 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 00000000..fe01c3fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 00000000..7bc7a5ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 00000000..cf1cf640 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 00000000..1d47959f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 00000000..3781dbd5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,923 @@ +/* + * 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.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.andmore.base.resources.ImageFactory; +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; + private ImageFactory mImageFactory; + + public DeviceExplorer(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + /** + * 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()); + + if (mFileImage == null) { + mFileImage = mImageFactory.getImageByName("file.png"); + } + if (mFolderImage == null) { + mFolderImage = mImageFactory.getImageByName("folder.png"); + } + if (mPackageImage == null) { + mPackageImage = mImageFactory.getImageByName("android.png"); + } + if (mOtherImage == null) { + // TODO: find a default image for other. + } + + mTree = new Tree(parent, SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL); + mTree.setHeaderVisible(true); + + IPreferenceStore store = DdmUiPreferences.getStore(); + + // create columns + TableHelper.createTreeColumn(mTree, "Name", SWT.LEFT, + "0000drwxrwxrwx", COLUMN_NAME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Size", SWT.RIGHT, + "000000", COLUMN_SIZE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Date", SWT.LEFT, + "2007-08-14", COLUMN_DATE, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Time", SWT.LEFT, + "20:54", COLUMN_TIME, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Permissions", SWT.LEFT, + "drwxrwxrwx", COLUMN_PERMISSIONS, store); //$NON-NLS-1$ + TableHelper.createTreeColumn(mTree, "Info", SWT.LEFT, + "drwxrwxrwx", COLUMN_INFO, store); //$NON-NLS-1$ + + // create the jface wrapper + mTreeViewer = new TreeViewer(mTree); + + // setup data provider + mContentProvider = new DeviceContentProvider(); + mTreeViewer.setContentProvider(mContentProvider); + mTreeViewer.setLabelProvider(new FileLabelProvider(mFileImage, + mFolderImage, mPackageImage, mOtherImage)); + + // setup a listener for selection + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + ISelection sel = event.getSelection(); + if (sel.isEmpty()) { + mPullAction.setEnabled(false); + mPushAction.setEnabled(false); + mDeleteAction.setEnabled(false); + mCreateNewFolderAction.setEnabled(false); + return; + } + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof FileEntry) { + mPullAction.setEnabled(true); + mPushAction.setEnabled(selection.size() == 1); + if (selection.size() == 1) { + FileEntry entry = (FileEntry) element; + setDeleteEnabledState(entry); + mCreateNewFolderAction.setEnabled(entry.isDirectory()); + } else { + mDeleteAction.setEnabled(false); + } + } + } + } + }); + + // add support for double click + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + public void doubleClick(DoubleClickEvent event) { + ISelection sel = event.getSelection(); + + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + + if (selection.size() == 1) { + FileEntry entry = (FileEntry)selection.getFirstElement(); + String name = entry.getName(); + + FileEntry parentEntry = entry.getParent(); + + // can't really do anything with no parent + if (parentEntry == null) { + return; + } + + // check this is a file like we want. + Matcher m = mKeyFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the data extension + String dataName = baseName + TRACE_DATA_EXT; + + FileEntry dataEntry = parentEntry.findChild(dataName); + + handleTraceDoubleClick(baseName, entry, dataEntry); + + } else { + m = mDataFilePattern.matcher(name); + if (m.matches()) { + // get the name w/o the extension + String baseName = m.group(1); + + // add the key extension + String keyName = baseName + TRACE_KEY_EXT; + + FileEntry keyEntry = parentEntry.findChild(keyName); + + handleTraceDoubleClick(baseName, keyEntry, entry); + } + } + } + } + } + }); + + // setup drop listener + mTreeViewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, + new Transfer[] { FileTransfer.getInstance() }, + new ViewerDropAdapter(mTreeViewer) { + @Override + public boolean performDrop(Object data) { + // get the item on which we dropped the item(s) + FileEntry target = (FileEntry)getCurrentTarget(); + + // in case we drop at the same level as root + if (target == null) { + return false; + } + + // if the target is not a directory, we get the parent directory + if (target.isDirectory() == false) { + target = target.getParent(); + } + + if (target == null) { + return false; + } + + // get the list of files to drop + String[] files = (String[])data; + + // do the drop + pushFiles(files, target); + + // we need to finish with a refresh + refresh(target); + + return true; + } + + @Override + public boolean validateDrop(Object target, int operation, TransferData transferType) { + if (target == null) { + return false; + } + + // convert to the real item + FileEntry targetEntry = (FileEntry)target; + + // if the target is not a directory, we get the parent directory + if (targetEntry.isDirectory() == false) { + target = targetEntry.getParent(); + } + + if (target == null) { + return false; + } + + return true; + } + }); + + // create and start the refresh thread + new Thread("Device Ls refresher") { + @Override + public void run() { + while (true) { + try { + sleep(FileListingService.REFRESH_RATE); + } catch (InterruptedException e) { + return; + } + + if (mTree != null && mTree.isDisposed() == false) { + Display display = mTree.getDisplay(); + if (display.isDisposed() == false) { + display.asyncExec(new Runnable() { + @Override + public void run() { + if (mTree.isDisposed() == false) { + mTreeViewer.refresh(true); + } + } + }); + } else { + return; + } + } else { + return; + } + } + + } + }.start(); + + return mTree; + } + + @Override + protected void postCreation() { + + } + + /** + * Sets the focus to the proper control inside the panel. + */ + @Override + public void setFocus() { + mTree.setFocus(); + } + + /** + * Processes a double click on a trace file + * @param baseName the base name of the 2 files. + * @param keyEntry The FileEntry for the .key file. + * @param dataEntry The FileEntry for the .data file. + */ + private void handleTraceDoubleClick(String baseName, FileEntry keyEntry, + FileEntry dataEntry) { + // first we need to download the files. + File keyFile; + File dataFile; + String path; + try { + // create a temp file for keyFile + File f = File.createTempFile(baseName, DdmConstants.DOT_TRACE); + f.delete(); + f.mkdir(); + + path = f.getAbsolutePath(); + + keyFile = new File(path + File.separator + keyEntry.getName()); + dataFile = new File(path + File.separator + dataEntry.getName()); + } catch (IOException e) { + return; + } + + // download the files + try { + SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + ISyncProgressMonitor monitor = SyncService.getNullProgressMonitor(); + sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); + sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); + + // now that we have the file, we need to launch traceview + String[] command = new String[2]; + command[0] = DdmUiPreferences.getTraceview(); + command[1] = path + File.separator + baseName; + + try { + final Process p = Runtime.getRuntime().exec(command); + + // create a thread for the output + new Thread("Traceview output") { + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(p.getErrorStream()); + BufferedReader resultReader = new BufferedReader(is); + + // read the lines as they come. if null is returned, it's + // because the process finished + try { + while (true) { + String line = resultReader.readLine(); + if (line != null) { + DdmConsole.printErrorToConsole("Traceview: " + line); + } else { + break; + } + } + // get the return code from the process + p.waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + + } + } + }.start(); + + } catch (IOException e) { + } + } + } catch (IOException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + return; + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + return; + } + } catch (TimeoutException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: timeout", keyEntry.getName())); + } catch (AdbCommandRejectedException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + } + } + + /** + * Pull the current selection on the local drive. This method displays + * a dialog box to let the user select where to store the file(s) and + * folder(s). + */ + public void pullSelection() { + // get the selection + TreeItem[] items = mTree.getSelection(); + + // name of the single file pull, or null if we're pulling a directory + // or more than one object. + String filePullName = null; + FileEntry singleEntry = null; + + // are we pulling a single file? + if (items.length == 1) { + singleEntry = (FileEntry)items[0].getData(); + if (singleEntry.getType() == FileListingService.TYPE_FILE) { + filePullName = singleEntry.getName(); + } + } + + // where do we save by default? + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + + if (filePullName != null) { + FileDialog fileDialog = new FileDialog(mParent.getShell(), SWT.SAVE); + + fileDialog.setText("Get Device File"); + fileDialog.setFileName(filePullName); + fileDialog.setFilterPath(defaultPath); + + String fileName = fileDialog.open(); + if (fileName != null) { + mDefaultSave = fileDialog.getFilterPath(); + + pullFile(singleEntry, fileName); + } + } else { + DirectoryDialog directoryDialog = new DirectoryDialog(mParent.getShell(), SWT.SAVE); + + directoryDialog.setText("Get Device Files/Folders"); + directoryDialog.setFilterPath(defaultPath); + + String directoryName = directoryDialog.open(); + if (directoryName != null) { + pullSelection(items, directoryName); + } + } + } + + /** + * Push new file(s) and folder(s) into the current selection. Current + * selection must be single item. If the current selection is not a + * directory, the parent directory is used. + * This method displays a dialog to let the user choose file to push to + * the device. + */ + public void pushIntoSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length == 0) { + return; + } + + FileDialog dlg = new FileDialog(mParent.getShell(), SWT.OPEN); + String fileName; + + dlg.setText("Put File on Device"); + + // There should be only one. + FileEntry entry = (FileEntry)items[0].getData(); + dlg.setFileName(entry.getName()); + + String defaultPath = mDefaultSave; + if (defaultPath == null) { + defaultPath = System.getProperty("user.home"); //$NON-NLS-1$ + } + dlg.setFilterPath(defaultPath); + + fileName = dlg.open(); + if (fileName != null) { + mDefaultSave = dlg.getFilterPath(); + + // we need to figure out the remote path based on the current selection type. + String remotePath; + FileEntry toRefresh = entry; + if (entry.isDirectory()) { + remotePath = entry.getFullPath(); + } else { + toRefresh = entry.getParent(); + remotePath = toRefresh.getFullPath(); + } + + pushFile(fileName, remotePath); + mTreeViewer.refresh(toRefresh); + } + } + + public void deleteSelection() { + // get the name of the object we're going to pull + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + FileEntry entry = (FileEntry)items[0].getData(); + final FileEntry parentEntry = entry.getParent(); + + // create the delete command + String command = "rm " + entry.getFullEscapedPath(); //$NON-NLS-1$ + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + @Override + public void addOutput(byte[] data, int offset, int length) { + // pass + // TODO get output to display errors if any. + } + + @Override + public void flush() { + mTreeViewer.refresh(parentEntry); + } + + @Override + public boolean isCancelled() { + return false; + } + }); + } catch (IOException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } catch (TimeoutException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } catch (AdbCommandRejectedException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } catch (ShellCommandUnresponsiveException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } + + } + + public void createNewFolderInSelection() { + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + final FileEntry entry = (FileEntry) items[0].getData(); + + if (entry.isDirectory()) { + InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder", + "Please enter the new folder name", "New Folder", new IInputValidator() { + @Override + public String isValid(String newText) { + if ((newText != null) && (newText.length() > 0) + && (newText.trim().length() > 0) + && (newText.indexOf('/') == -1) + && (newText.indexOf('\\') == -1)) { + return null; + } else { + return "Invalid name"; + } + } + }); + inputDialog.open(); + String value = inputDialog.getValue(); + + if (value != null) { + // create the mkdir command + String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$ + + FileListingService.FILE_SEPARATOR + FileEntry.escape(value); + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void flush() { + mTreeViewer.refresh(entry); + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + String errorMessage; + if (data != null) { + errorMessage = new String(data); + } else { + errorMessage = ""; + } + Status status = new Status(IStatus.ERROR, + "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$ + ErrorDialog.openError(mTree.getShell(), "New Folder Error", + "New Folder Error", status); + } + }); + } catch (TimeoutException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } catch (AdbCommandRejectedException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } catch (ShellCommandUnresponsiveException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } catch (IOException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } + } + } + } + + /** + * Force a full refresh of the explorer. + */ + public void refresh() { + mTreeViewer.refresh(true); + } + + /** + * Sets the new device to explorer + */ + public void switchDevice(final IDevice device) { + if (device != mCurrentDevice) { + mCurrentDevice = device; + // now we change the input. but we need to do that in the + // ui thread. + if (mTree.isDisposed() == false) { + Display d = mTree.getDisplay(); + d.asyncExec(new Runnable() { + @Override + public void run() { + if (mTree.isDisposed() == false) { + // new service + if (mCurrentDevice != null) { + FileListingService fls = mCurrentDevice.getFileListingService(); + mContentProvider.setListingService(fls); + mTreeViewer.setInput(fls.getRoot()); + } + } + } + }); + } + } + } + + /** + * Refresh an entry from a non ui thread. + * @param entry the entry to refresh. + */ + private void refresh(final FileEntry entry) { + Display d = mTreeViewer.getTree().getDisplay(); + d.asyncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(entry); + } + }); + } + + /** + * Pulls the selection from a device. + * @param items the tree selection the remote file on the device + * @param localDirector the local directory in which to save the files. + */ + private void pullSelection(TreeItem[] items, final String localDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + // make a list of the FileEntry. + ArrayList entries = new ArrayList(); + for (TreeItem item : items) { + Object data = item.getData(); + if (data instanceof FileEntry) { + entries.add((FileEntry)data); + } + } + final FileEntry[] entryArray = entries.toArray( + new FileEntry[entries.size()]); + + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pull(entryArray, localDirectory, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, "Pulling file(s) from the device", mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", e.getMessage())); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pulls a file from a device. + * @param remote the remote file on the device + * @param local the destination filepath + */ + private void pullFile(final FileEntry remote, final String local) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pullFile(remote, local, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, String.format("Pulling %1$s from the device", remote.getName()), + mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", e.getMessage())); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole( "Failed to pull selection"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pushes several files and directory into a remote directory. + * @param localFiles + * @param remoteDirectory + */ + private void pushFiles(final String[] localFiles, final FileEntry remoteDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.push(localFiles, remoteDirectory, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, "Pushing file(s) to the device", mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push selection: %1$s", e.getMessage())); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole("Failed to push the items"); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Pushes a file on a device. + * @param local the local filepath of the file to push + * @param remoteDirectory the remote destination directory on the device + */ + private void pushFile(final String local, final String remoteDirectory) { + try { + final SyncService sync = mCurrentDevice.getSyncService(); + if (sync != null) { + // get the file name + String[] segs = local.split(Pattern.quote(File.separator)); + String name = segs[segs.length-1]; + final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + + name; + + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pushFile(local, remoteFile, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, String.format("Pushing %1$s to the device.", name), mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push selection: %1$s", e.getMessage())); + } + } catch (Exception e) { + DdmConsole.printErrorToConsole("Failed to push the item(s)."); + DdmConsole.printErrorToConsole(e.getMessage()); + } + } + + /** + * Sets the enabled state based on a FileEntry properties + * @param element The selected FileEntry + */ + protected void setDeleteEnabledState(FileEntry element) { + mDeleteAction.setEnabled(element.getType() == FileListingService.TYPE_FILE); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 00000000..36a51c81 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java new file mode 100644 index 00000000..1e715c76 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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

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

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

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

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

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

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

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

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

This value can be erroneous, and a check with {@link #isValueValid()} should be performed + * before any call to this method. + */ + public double getValue() { + return mValue; + } + + /** + * Enables or disables all the {@link Text} controls. + * @param enabled the enabled state. + */ + public void setEnabled(boolean enabled) { + mDecimalText.setEnabled(enabled); + mSexagesimalDegreeText.setEnabled(enabled); + mSexagesimalMinuteText.setEnabled(enabled); + mSexagesimalSecondText.setEnabled(enabled); + } + + private void resetDecimalControls() { + mManualTextChange++; + mDecimalText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + private void resetSexagesimalControls() { + mManualTextChange++; + mSexagesimalDegreeText.setText(""); //$NON-NLS-1$ + mSexagesimalMinuteText.setText(""); //$NON-NLS-1$ + mSexagesimalSecondText.setText(""); //$NON-NLS-1$ + mManualTextChange--; + } + + /** + * Creates a {@link Text} with a given parent, default string and a {@link ModifyListener} + * @param parent the parent {@link Composite}. + * @param defaultString the default string to be used to compute the {@link Text} control + * size hint. + * @param listener the {@link ModifyListener} to be called when the {@link Text} control is + * modified. + */ + private Text createTextControl(Composite parent, String defaultString, + ModifyListener listener) { + // create the control + Text text = new Text(parent, SWT.BORDER | SWT.LEFT | SWT.SINGLE); + + // add the standard listener to it. + text.addModifyListener(listener); + + // compute its size/ + mManualTextChange++; + text.setText(defaultString); + text.pack(); + Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT); + text.setText(""); //$NON-NLS-1$ + mManualTextChange--; + + GridData gridData = new GridData(); + gridData.widthHint = size.x; + text.setLayoutData(gridData); + + return text; + } + + private double getValueFromSexagesimalControls() throws ParseException { + double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue(); + double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue(); + double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue(); + + boolean isPositive = (degrees >= 0.); + degrees = Math.abs(degrees); + + double value = degrees + minutes / 60. + seconds / 3600.; + return isPositive ? value : - value; + } + + private void setValueIntoDecimalControl(double value) { + mManualTextChange++; + mDecimalText.setText(String.format("%.6f", value)); + mManualTextChange--; + } + + private void setValueIntoSexagesimalControl(double value) { + // get the sign and make the number positive no matter what. + boolean isPositive = (value >= 0.); + value = Math.abs(value); + + // get the degree + double degrees = Math.floor(value); + + // get the minutes + double minutes = Math.floor((value - degrees) * 60.); + + // get the seconds. + double seconds = (value - degrees) * 3600. - minutes * 60.; + + mManualTextChange++; + mSexagesimalDegreeText.setText( + Integer.toString(isPositive ? (int)degrees : (int)- degrees)); + mSexagesimalMinuteText.setText(Integer.toString((int)minutes)); + mSexagesimalSecondText.setText(String.format("%.3f", seconds)); //$NON-NLS-1$ + mManualTextChange--; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 00000000..bb4da6c6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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

A track is composed of a list of {@link TrackPoint} and optional name and comment. + */ + public final static class Track { + private String mName; + private String mComment; + private List mPoints = new ArrayList(); + + void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + void setComment(String comment) { + mComment = comment; + } + + public String getComment() { + return mComment; + } + + void addPoint(TrackPoint trackPoint) { + mPoints.add(trackPoint); + } + + public TrackPoint[] getPoints() { + return mPoints.toArray(new TrackPoint[mPoints.size()]); + } + + public long getFirstPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(0).getTime(); + } + + return -1; + } + + public long getLastPointTime() { + if (mPoints.size() > 0) { + return mPoints.get(mPoints.size()-1).getTime(); + } + + return -1; + } + + public int getPointCount() { + return mPoints.size(); + } + } + + /** + * Creates a new GPX parser for a file specified by its full path. + * @param fileName The full path of the GPX file to parse. + */ + public GpxParser(String fileName) { + mFileName = fileName; + } + + /** + * Parses the GPX file. + * @return true if success. + */ + public boolean parse() { + try { + SAXParser parser = sParserFactory.newSAXParser(); + + mHandler = new GpxHandler(); + + parser.parse(new InputSource(new FileReader(mFileName)), mHandler); + + return mHandler.getSuccess(); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } finally { + } + + return false; + } + + /** + * Returns the parsed {@link WayPoint} objects, or null if none were found (or + * if the parsing failed. + */ + public WayPoint[] getWayPoints() { + if (mHandler != null) { + return mHandler.getWayPoints(); + } + + return null; + } + + /** + * Returns the parsed {@link Track} objects, or null if none were found (or + * if the parsing failed. + */ + public Track[] getTracks() { + if (mHandler != null) { + return mHandler.getTracks(); + } + + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 00000000..f04200fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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

The expected type for the input is {@link Track}[]. + */ +public class TrackContentProvider implements IStructuredContentProvider { + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Track[]) { + return (Track[])inputElement; + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 00000000..c1ce106e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 00000000..ef8a3170 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 00000000..527d522f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 00000000..b60062fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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. + *

The expected type for the input is {@link WayPoint}[]. + */ +public class WayPointContentProvider implements IStructuredContentProvider { + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof WayPoint[]) { + return (WayPoint[])inputElement; + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 00000000..a8a75aa3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 00000000..55e64d04 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 content = new ArrayList(); + while ((line = reader.readLine()) != null) { + if (LOG_HEADER.equals(line)) { + mTags = content.toArray(new String[content.size()]); + readLog(reader); + return; + } else { + content.add(line); + } + } + } + + private void readLog(BufferedReader reader) throws IOException { + String line; + + ArrayList content = new ArrayList(); + while ((line = reader.readLine()) != null) { + if (line.startsWith(HEADER_TAG) == false) { + content.add(line); + } else { + break; + } + } + + mLog = content.toArray(new String[content.size()]); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 00000000..5b7f40db --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + addToLog(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_FILTERED_LOG; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 00000000..e6d6509e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 datasets = mValueTypeDataSetMap.values(); + for (TimeSeriesCollection dataset : datasets) { + dataset.removeAllSeries(); + } + if (mOccurrenceDataSet != null) { + mOccurrenceDataSet.removeAllSeries(); + } + mValueDescriptorSeriesMap.clear(); + mOcurrenceDescriptorSeriesMap.clear(); + } + + /** + * Creates the UI for the event display. + * @param parent the parent composite. + * @param logParser the current log parser. + * @return the created control (which may have children). + */ + @Override + public Control createComposite(final Composite parent, EventLogParser logParser, + final ILogColumnListener listener) { + String title = getChartTitle(logParser); + return createCompositeChart(parent, logParser, title); + } + + /** + * Adds event to the display. + */ + @Override + void newEvent(EventContainer event, EventLogParser logParser) { + ArrayList valueDescriptors = + new ArrayList(); + + ArrayList occurrenceDescriptors = + new ArrayList(); + + if (filterEvent(event, valueDescriptors, occurrenceDescriptors)) { + updateChart(event, logParser, valueDescriptors, occurrenceDescriptors); + } + } + + /** + * Updates the chart with the {@link EventContainer} by adding the values/occurrences defined + * by the {@link ValueDisplayDescriptor} and {@link OccurrenceDisplayDescriptor} objects from + * the two lists. + *

This method is only called when at least one of the descriptor list is non empty. + * @param event + * @param logParser + * @param valueDescriptors + * @param occurrenceDescriptors + */ + private void updateChart(EventContainer event, EventLogParser logParser, + ArrayList valueDescriptors, + ArrayList occurrenceDescriptors) { + Map tagMap = logParser.getTagMap(); + + Millisecond millisecondTime = null; + long msec = -1; + + // If the event container is a cpu container (tag == 2721), and there is no descriptor + // for the total CPU load, then we do accumulate all the values. + boolean accumulateValues = false; + double accumulatedValue = 0; + + if (event.mTag == 2721) { + accumulateValues = true; + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + accumulateValues &= (descriptor.valueIndex != 0); + } + } + + for (ValueDisplayDescriptor descriptor : valueDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mValueDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mValueDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it + if (timeSeries == null) { + // get the series name + String seriesFullName = null; + String seriesLabel = getSeriesLabel(event, descriptor); + + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + seriesFullName = String.format("%1$s / %2$s", seriesLabel, + descriptor.valueName); + break; + case EVENT_CHECK_SAME_VALUE: + seriesFullName = String.format("%1$s", seriesLabel); + break; + default: + seriesFullName = String.format("%1$s / %2$s: %3$s", seriesLabel, + tagMap.get(descriptor.eventTag), + descriptor.valueName); + break; + } + + // get the data set for this ValueType + TimeSeriesCollection dataset = getValueDataset( + logParser.getEventInfoMap().get(event.mTag)[descriptor.valueIndex] + .getValueType(), + accumulateValues); + + // create the series + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge * 1000); + } + + dataset.addSeries(timeSeries); + + // add it to the map. + map.put(event.pid, timeSeries); + } + + // update the timeSeries. + + // get the value from the event + double value = event.getValueAsDouble(descriptor.valueIndex); + + // accumulate the values if needed. + if (accumulateValues) { + accumulatedValue += value; + value = accumulatedValue; + } + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, value); + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + for (OccurrenceDisplayDescriptor descriptor : occurrenceDescriptors) { + try { + // get the hashmap for this descriptor + HashMap map = mOcurrenceDescriptorSeriesMap.get(descriptor); + + // if it's not there yet, we create it. + if (map == null) { + map = new HashMap(); + mOcurrenceDescriptorSeriesMap.put(descriptor, map); + } + + // get the TimeSeries for this pid + TimeSeries timeSeries = map.get(event.pid); + + // if it doesn't exist yet, we create it. + if (timeSeries == null) { + String seriesLabel = getSeriesLabel(event, descriptor); + + String seriesFullName = String.format("[%1$s:%2$s]", + tagMap.get(descriptor.eventTag), seriesLabel); + + timeSeries = new TimeSeries(seriesFullName, Millisecond.class); + if (mMaximumChartItemAge != -1) { + timeSeries.setMaximumItemAge(mMaximumChartItemAge); + } + + getOccurrenceDataSet().addSeries(timeSeries); + + map.put(event.pid, timeSeries); + } + + // update the series + + // get the time + if (millisecondTime == null) { + msec = (long)event.sec * 1000L + (event.nsec / 1000000L); + millisecondTime = new Millisecond(new Date(msec)); + } + + // add the value to the time series + timeSeries.addOrUpdate(millisecondTime, 0); // the value is unused + } catch (InvalidTypeException e) { + // just ignore this descriptor if there's a type mismatch + } + } + + // go through all the series and remove old values. + if (msec != -1 && mMaximumChartItemAge != -1) { + Collection> pidMapValues = + mValueDescriptorSeriesMap.values(); + + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + + pidMapValues = mOcurrenceDescriptorSeriesMap.values(); + for (HashMap pidMapValue : pidMapValues) { + Collection seriesCollection = pidMapValue.values(); + + for (TimeSeries timeSeries : seriesCollection) { + timeSeries.removeAgedItems(msec, true); + } + } + } + } + + /** + * Returns a {@link TimeSeriesCollection} for a specific {@link com.android.ddmlib.log.EventValueDescription.ValueType}. + * If the data set is not yet created, it is first allocated and set up into the + * {@link org.jfree.chart.JFreeChart} object. + * @param type the {@link com.android.ddmlib.log.EventValueDescription.ValueType} of the data set. + * @param accumulateValues + */ + private TimeSeriesCollection getValueDataset(EventValueDescription.ValueType type, boolean accumulateValues) { + TimeSeriesCollection dataset = mValueTypeDataSetMap.get(type); + if (dataset == null) { + // create the data set and store it in the map + dataset = new TimeSeriesCollection(); + mValueTypeDataSetMap.put(type, dataset); + + // create the renderer and configure it depending on the ValueType + AbstractXYItemRenderer renderer; + if (type == EventValueDescription.ValueType.PERCENT && accumulateValues) { + renderer = new XYAreaRenderer(); + } else { + XYLineAndShapeRenderer r = new XYLineAndShapeRenderer(); + r.setBaseShapesVisible(type != EventValueDescription.ValueType.PERCENT); + + renderer = r; + } + + // set both the dataset and the renderer in the plot object. + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, dataset); + xyPlot.setRenderer(mDataSetCount, renderer); + + // put a new axis label, and configure it. + NumberAxis axis = new NumberAxis(type.toString()); + + if (type == EventValueDescription.ValueType.PERCENT) { + // force percent range to be (0,100) fixed. + axis.setAutoRange(false); + axis.setRange(0., 100.); + } + + // for the index, we ignore the occurrence dataset + int count = mDataSetCount; + if (mOccurrenceDataSet != null) { + count--; + } + + xyPlot.setRangeAxis(count, axis); + if ((count % 2) == 0) { + xyPlot.setRangeAxisLocation(count, AxisLocation.BOTTOM_OR_LEFT); + } else { + xyPlot.setRangeAxisLocation(count, AxisLocation.TOP_OR_RIGHT); + } + + // now we link the dataset and the axis + xyPlot.mapDatasetToRangeAxis(mDataSetCount, count); + + mDataSetCount++; + } + + return dataset; + } + + /** + * Return the series label for this event. This only contains the pid information. + * @param event the {@link EventContainer} + * @param descriptor the {@link OccurrenceDisplayDescriptor} + * @return the series label. + * @throws InvalidTypeException + */ + private String getSeriesLabel(EventContainer event, OccurrenceDisplayDescriptor descriptor) + throws InvalidTypeException { + if (descriptor.seriesValueIndex != -1) { + if (descriptor.includePid == false) { + return event.getValueAsString(descriptor.seriesValueIndex); + } else { + return String.format("%1$s (%2$d)", + event.getValueAsString(descriptor.seriesValueIndex), event.pid); + } + } + + return Integer.toString(event.pid); + } + + /** + * Returns the {@link TimeSeriesCollection} for the occurrence display. If the data set is not + * yet created, it is first allocated and set up into the {@link org.jfree.chart.JFreeChart} object. + */ + private TimeSeriesCollection getOccurrenceDataSet() { + if (mOccurrenceDataSet == null) { + mOccurrenceDataSet = new TimeSeriesCollection(); + + XYPlot xyPlot = mChart.getXYPlot(); + xyPlot.setDataset(mDataSetCount, mOccurrenceDataSet); + + OccurrenceRenderer renderer = new OccurrenceRenderer(); + renderer.setBaseShapesVisible(false); + xyPlot.setRenderer(mDataSetCount, renderer); + + mDataSetCount++; + } + + return mOccurrenceDataSet; + } + + /** + * Gets display type + * + * @return display type as an integer + */ + @Override + int getDisplayType() { + return DISPLAY_TYPE_GRAPH; + } + + /** + * Sets the current {@link EventLogParser} object. + */ + @Override + protected void setNewLogParser(EventLogParser logParser) { + if (mChart != null) { + mChart.setTitle(getChartTitle(logParser)); + } + } + /** + * Returns a meaningful chart title based on the value of {@link #mValueDescriptorCheck}. + * + * @param logParser the logParser. + * @return the chart title. + */ + private String getChartTitle(EventLogParser logParser) { + if (mValueDescriptors.size() > 0) { + String chartDesc = null; + switch (mValueDescriptorCheck) { + case EVENT_CHECK_SAME_TAG: + if (logParser != null) { + chartDesc = logParser.getTagMap().get(mValueDescriptors.get(0).eventTag); + } + break; + case EVENT_CHECK_SAME_VALUE: + if (logParser != null) { + chartDesc = String.format("%1$s / %2$s", + logParser.getTagMap().get(mValueDescriptors.get(0).eventTag), + mValueDescriptors.get(0).valueName); + } + break; + } + + if (chartDesc != null) { + return String.format("%1$s - %2$s", mName, chartDesc); + } + } + + return mName; + } +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 00000000..89a145f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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). + *

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

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

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

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

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

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

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

+ * This will run asynchronously in the UI thread. + */ + @WorkerThread + private void updateEventDisplays() { + try { + Display d = mBottomParentPanel.getDisplay(); + + d.asyncExec(new Runnable() { + @Override + public void run() { + if (mBottomParentPanel.isDisposed() == false) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.setNewLogParser(mCurrentEventLogParser); + } + + mOptionsAction.setEnabled(true); + mClearAction.setEnabled(true); + if (mCurrentLogFile == null) { + mSaveAction.setEnabled(true); + } else { + mSaveAction.setEnabled(false); + } + } + } + }); + } catch (SWTException e) { + // display is disposed: do nothing. + } + } + + @Override + @UiThread + public void columnResized(int index, TableColumn sourceColumn) { + for (EventDisplay eventDisplay : mEventDisplays) { + eventDisplay.resizeColumn(index, sourceColumn); + } + } + + /** + * Runs an event log service out of a local file. + * @param fileName the full file name of the local file containing the event log. + * @param logReceiver the receiver that will handle the log + * @throws IOException + */ + @WorkerThread + private void runLocalEventLogService(String fileName, LogReceiver logReceiver) + throws IOException { + byte[] buffer = new byte[256]; + + FileInputStream fis = new FileInputStream(fileName); + try { + int count; + while ((count = fis.read(buffer)) != -1) { + logReceiver.parseNewData(buffer, 0, count); + } + } finally { + fis.close(); + } + } + + @WorkerThread + private void runLocalEventLogService(String[] log, LogReceiver currentLogReceiver) { + synchronized (mLock) { + for (String line : log) { + EventContainer event = mCurrentEventLogParser.parse(line); + if (event != null) { + handleNewEvent(event); + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 00000000..6361d6b0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mSeriesIndices = new ArrayList(); + + public EventValueSelector(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * Opens the display option dialog to edit a new descriptor. + * @param decriptorClass the class of the object to instantiate. Must extend + * {@link OccurrenceDisplayDescriptor} + * @param logParser + * @return true if the object is to be created, false if the creation was canceled. + */ + boolean open(Class descriptorClass, + EventLogParser logParser) { + try { + OccurrenceDisplayDescriptor descriptor = descriptorClass.newInstance(); + setModified(); + return open(descriptor, logParser); + } catch (InstantiationException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } + } + + /** + * Opens the display option dialog, to edit a {@link OccurrenceDisplayDescriptor} object or + * a {@link ValueDisplayDescriptor} object. + * @param descriptor The descriptor to edit. + * @return true if the object was modified. + */ + boolean open(OccurrenceDisplayDescriptor descriptor, EventLogParser logParser) { + // make a copy of the descriptor as we'll use a working copy. + if (descriptor instanceof ValueDisplayDescriptor) { + mDescriptor = new ValueDisplayDescriptor((ValueDisplayDescriptor)descriptor); + } else if (descriptor instanceof OccurrenceDisplayDescriptor) { + mDescriptor = new OccurrenceDisplayDescriptor(descriptor); + } else { + return false; + } + + mLogParser = logParser; + + createUI(); + + if (mParent == null || mShell == null) { + return false; + } + + loadValueDescriptor(); + + checkValidity(); + + // Set the dialog size. + try { + mShell.setMinimumSize(DLG_WIDTH, DLG_HEIGHT); + Rectangle r = mParent.getBounds(); + // get the center new top left. + int cx = r.x + r.width/2; + int x = cx - DLG_WIDTH / 2; + int cy = r.y + r.height/2; + int y = cy - DLG_HEIGHT / 2; + mShell.setBounds(x, y, DLG_WIDTH, DLG_HEIGHT); + } catch (Exception e) { + e.printStackTrace(); + } + + mShell.layout(); + + // actually open the dialog + mShell.open(); + + // event loop until the dialog is closed. + Display display = mParent.getDisplay(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + return mEditStatus; + } + + OccurrenceDisplayDescriptor getDescriptor() { + return mDescriptor; + } + + private void createUI() { + GridData gd; + + mParent = getParent(); + mShell = new Shell(mParent, getStyle()); + mShell.setText("Event Display Configuration"); + + mShell.setLayout(new GridLayout(2, false)); + + Label l = new Label(mShell, SWT.NONE); + l.setText("Event:"); + + mEventCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mEventCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // the event tag / event name map + Map eventTagMap = mLogParser.getTagMap(); + Map eventInfoMap = mLogParser.getEventInfoMap(); + Set keys = eventTagMap.keySet(); + ArrayList list = new ArrayList(); + for (Integer i : keys) { + if (eventInfoMap.get(i) != null) { + String eventName = eventTagMap.get(i); + mEventCombo.add(eventName); + + list.add(i); + } + } + mEventTags = list.toArray(new Integer[list.size()]); + + mEventCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleEventComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Value:"); + + mValueCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mValueCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mValueCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleValueComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Series Name:"); + + mSeriesCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mSeriesCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSeriesCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleSeriesComboSelection(); + setModified(); + } + }); + + // empty comp + new Composite(mShell, SWT.NONE).setLayoutData(gd = new GridData()); + gd.heightHint = gd.widthHint = 0; + + mDisplayPidCheckBox = new Button(mShell, SWT.CHECK); + mDisplayPidCheckBox.setText("Also Show pid"); + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mDescriptor.includePid = mDisplayPidCheckBox.getSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter By:"); + + mFilterCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Method:"); + + mFilterMethodCombo = new Combo(mShell, SWT.DROP_DOWN | SWT.READ_ONLY); + mFilterMethodCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + for (CompareMethod method : CompareMethod.values()) { + mFilterMethodCombo.add(method.toString()); + } + mFilterMethodCombo.select(0); + mFilterMethodCombo.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleFilterMethodComboSelection(); + setModified(); + } + }); + + l = new Label(mShell, SWT.NONE); + l.setText("Filter Value:"); + + mFilterValue = new Text(mShell, SWT.BORDER | SWT.SINGLE); + mFilterValue.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterValue.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + if (mDescriptor.filterValueIndex != -1) { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription valueDesc = mLogParser.getEventInfoMap() + .get(eventTag)[mDescriptor.filterValueIndex]; + + // let the EventValueDescription convert the String value into an object + // of the proper type. + mDescriptor.filterValue = valueDesc.getObjectFromString( + mFilterValue.getText().trim()); + setModified(); + } + } + } + }); + + // add a separator spanning the 2 columns + + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + + // add a composite to hold the ok/cancel button, no matter what the columns size are. + Composite buttonComp = new Composite(mShell, SWT.NONE); + gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + buttonComp.setLayoutData(gd); + GridLayout gl; + buttonComp.setLayout(gl = new GridLayout(6, true)); + gl.marginHeight = gl.marginWidth = 0; + + Composite padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mOkButton = new Button(buttonComp, SWT.PUSH); + mOkButton.setText("OK"); + mOkButton.setLayoutData(new GridData(GridData.CENTER)); + mOkButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Button cancelButton = new Button(buttonComp, SWT.PUSH); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(GridData.CENTER)); + cancelButton.addSelectionListener(new SelectionAdapter() { + /* (non-Javadoc) + * @see org.eclipse.swt.events.SelectionAdapter#widgetSelected(org.eclipse.swt.events.SelectionEvent) + */ + @Override + public void widgetSelected(SelectionEvent e) { + // cancel the edit + mEditStatus = false; + mShell.close(); + } + }); + + padding = new Composite(mShell, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mShell.addListener(SWT.Close, new Listener() { + @Override + public void handleEvent(Event event) { + event.doit = true; + } + }); + } + + private void setModified() { + mEditStatus = true; + } + + private void handleEventComboSelection() { + // get the current selection in the event combo + int index = mEventCombo.getSelectionIndex(); + + if (index != -1) { + // match it to an event + int eventTag = mEventTags[index]; + mDescriptor.eventTag = eventTag; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + // fill the combo for the values + mValueCombo.removeAll(); + if (values != null) { + if (mDescriptor instanceof ValueDisplayDescriptor) { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + mValueCombo.setEnabled(true); + for (EventValueDescription value : values) { + mValueCombo.add(value.toString()); + } + + if (valueDescriptor.valueIndex != -1) { + mValueCombo.select(valueDescriptor.valueIndex); + } else { + mValueCombo.clearSelection(); + } + } else { + mValueCombo.setEnabled(false); + } + + // fill the axis combo + mSeriesCombo.removeAll(); + mSeriesCombo.setEnabled(false); + mSeriesIndices.clear(); + int axisIndex = 0; + int selectionIndex = -1; + for (EventValueDescription value : values) { + if (value.getEventValueType() == EventValueType.STRING) { + mSeriesCombo.add(value.getName()); + mSeriesCombo.setEnabled(true); + mSeriesIndices.add(axisIndex); + + if (mDescriptor.seriesValueIndex != -1 && + mDescriptor.seriesValueIndex == axisIndex) { + selectionIndex = axisIndex; + } + } + axisIndex++; + } + + if (mSeriesCombo.isEnabled()) { + mSeriesCombo.add("default (pid)", 0 /* index */); + mSeriesIndices.add(0 /* index */, -1 /* value */); + + // +1 because we added another item at index 0 + mSeriesCombo.select(selectionIndex + 1); + + if (selectionIndex >= 0) { + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + mDisplayPidCheckBox.setEnabled(true); + } else { + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + } + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + + // fill the filter combo + mFilterCombo.setEnabled(true); + mFilterCombo.removeAll(); + mFilterCombo.add("(no filter)"); + for (EventValueDescription value : values) { + mFilterCombo.add(value.toString()); + } + + // select the current filter + mFilterCombo.select(mDescriptor.filterValueIndex + 1); + mFilterMethodCombo.select(getFilterMethodIndex(mDescriptor.filterCompareMethod)); + + // fill the current filter value + if (mDescriptor.filterValueIndex != -1) { + EventValueDescription valueInfo = values[mDescriptor.filterValueIndex]; + if (valueInfo.checkForType(mDescriptor.filterValue)) { + mFilterValue.setText(mDescriptor.filterValue.toString()); + } else { + mFilterValue.setText(""); + } + } else { + mFilterValue.setText(""); + } + } else { + disableSubCombos(); + } + } else { + disableSubCombos(); + } + + checkValidity(); + } + + /** + * + */ + private void disableSubCombos() { + mValueCombo.removeAll(); + mValueCombo.clearSelection(); + mValueCombo.setEnabled(false); + + mSeriesCombo.removeAll(); + mSeriesCombo.clearSelection(); + mSeriesCombo.setEnabled(false); + + mDisplayPidCheckBox.setEnabled(false); + mDisplayPidCheckBox.setSelection(false); + + mFilterCombo.removeAll(); + mFilterCombo.clearSelection(); + mFilterCombo.setEnabled(false); + + mFilterValue.setEnabled(false); + mFilterValue.setText(""); + mFilterMethodCombo.setEnabled(false); + } + + private void handleValueComboSelection() { + ValueDisplayDescriptor valueDescriptor = (ValueDisplayDescriptor)mDescriptor; + + // get the current selection in the value combo + int index = mValueCombo.getSelectionIndex(); + valueDescriptor.valueIndex = index; + + // for now set the built-in name + + // get the current selection in the event combo + int eventIndex = mEventCombo.getSelectionIndex(); + + // match it to an event + int eventTag = mEventTags[eventIndex]; + + // get the EventValueDescription for this tag + EventValueDescription[] values = mLogParser.getEventInfoMap().get(eventTag); + + valueDescriptor.valueName = values[index].getName(); + + checkValidity(); + } + + private void handleSeriesComboSelection() { + // get the current selection in the axis combo + int index = mSeriesCombo.getSelectionIndex(); + + // get the actual value index from the list. + int valueIndex = mSeriesIndices.get(index); + + mDescriptor.seriesValueIndex = valueIndex; + + if (index > 0) { + mDisplayPidCheckBox.setEnabled(true); + mDisplayPidCheckBox.setSelection(mDescriptor.includePid); + } else { + mDisplayPidCheckBox.setSelection(false); + mDisplayPidCheckBox.setEnabled(false); + } + } + + private void handleFilterComboSelection() { + // get the current selection in the axis combo + int index = mFilterCombo.getSelectionIndex(); + + // decrement index by 1 since the item 0 means + // no filter (index = -1), and the rest is offset by 1 + index--; + + mDescriptor.filterValueIndex = index; + + if (index != -1) { + mFilterValue.setEnabled(true); + mFilterMethodCombo.setEnabled(true); + if (mDescriptor.filterValue instanceof String) { + mFilterValue.setText((String)mDescriptor.filterValue); + } + } else { + mFilterValue.setText(""); + mFilterValue.setEnabled(false); + mFilterMethodCombo.setEnabled(false); + } + } + + private void handleFilterMethodComboSelection() { + // get the current selection in the axis combo + int index = mFilterMethodCombo.getSelectionIndex(); + CompareMethod method = CompareMethod.values()[index]; + + mDescriptor.filterCompareMethod = method; + } + + /** + * Returns the index of the filter method + * @param filterCompareMethod the {@link CompareMethod} enum. + */ + private int getFilterMethodIndex(CompareMethod filterCompareMethod) { + CompareMethod[] values = CompareMethod.values(); + for (int i = 0 ; i < values.length ; i++) { + if (values[i] == filterCompareMethod) { + return i; + } + } + return -1; + } + + + private void loadValueDescriptor() { + // get the index from the eventTag. + int eventIndex = 0; + int comboIndex = -1; + for (int i : mEventTags) { + if (i == mDescriptor.eventTag) { + comboIndex = eventIndex; + break; + } + eventIndex++; + } + + if (comboIndex == -1) { + mEventCombo.clearSelection(); + } else { + mEventCombo.select(comboIndex); + } + + // get the event from the descriptor + handleEventComboSelection(); + } + + private void checkValidity() { + mOkButton.setEnabled(mEventCombo.getSelectionIndex() != -1 && + (((mDescriptor instanceof ValueDisplayDescriptor) == false) || + mValueCombo.getSelectionIndex() != -1)); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 00000000..f3d8ece9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 00000000..729122e0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 00000000..ce9bc2fc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,383 @@ +/* + * 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.andmore.base.resources.ImageFactory; +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; + private ImageFactory mImageFactory; + + public EditFilterDialog(Shell parent, ImageFactory imageFactory) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageFactory = imageFactory; + } + + public EditFilterDialog(Shell shell, ImageFactory imageFactory, LogFilter filter) { + this(shell, imageFactory); + 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(mImageFactory.getImageByName(IMAGE_EMPTY)); + + // 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(mImageFactory.getImageByName(IMAGE_EMPTY)); + + 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(mImageFactory.getImageByName(IMAGE_EMPTY)); + + l = new Label(main, SWT.NONE); + l.setText("by Log level:"); + + final Combo logCombo = new Combo(main, SWT.DROP_DOWN | SWT.READ_ONLY); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + logCombo.setLayoutData(gd); + + // add the labels + logCombo.add(""); + logCombo.add("Error"); + logCombo.add("Warning"); + logCombo.add("Info"); + logCombo.add("Debug"); + logCombo.add("Verbose"); + + if (mFilter != null) { + mLogLevel = getComboIndex(mFilter.getLogLevel()); + logCombo.select(mLogLevel); + } else { + logCombo.select(0); + } + + logCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + // get the selection + mLogLevel = logCombo.getSelectionIndex(); + validate(); + } + }); + + // separator + l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // bottom part with the ok/cancel + Composite bottomComp = new Composite(mShell, SWT.NONE); + bottomComp + .setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); + bottomComp.setLayout(new GridLayout(2, true)); + + mOkButton = new Button(bottomComp, SWT.NONE); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mOk = true; + mShell.close(); + } + }); + mOkButton.setEnabled(false); + mShell.setDefaultButton(mOkButton); + + Button cancelButton = new Button(bottomComp, SWT.NONE); + cancelButton.setText("Cancel"); + cancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mShell.close(); + } + }); + + validate(); + } + + /** + * Returns the log level from a combo index. + * @param index the Combo index + * @return a log level valid for the Log class. + */ + protected int getLogLevel(int index) { + if (index == 0) { + return -1; + } + + return 7 - index; + } + + /** + * Returns the index in the combo that matches the log level + * @param logLevel The Log level. + * @return the combo index + */ + private int getComboIndex(int logLevel) { + if (logLevel == -1) { + return 0; + } + + return 7 - logLevel; + } + + /** + * Validates the content of the 2 text fields and enable/disable "ok", while + * setting up the warning/error message. + */ + private void validate() { + + boolean result = true; + + // then we check it only contains digits. + if (mPid != null) { + if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$ + mPidWarning.setImage(mImageFactory.getImageByName(IMAGE_WARNING)); + mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$ + result = false; + } else { + mPidWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); + mPidWarning.setToolTipText(null); + } + } + + // then we check it not contains character | or : + if (mTag != null) { + if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$ + mTagWarning.setImage(mImageFactory.getImageByName(IMAGE_WARNING)); + mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$ + result = false; + } else { + mTagWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); + 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(mImageFactory.getImageByName(IMAGE_WARNING)); + mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$ + result = false; + } else { + mNameWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); + mNameWarning.setToolTipText(null); + } + } else { + result = false; + } + + mOkButton.setEnabled(result); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java new file mode 100644 index 00000000..db586cca --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 addedMessages, List deletedMessages); +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java new file mode 100644 index 00000000..60c6d054 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java new file mode 100644 index 00000000..329c2029 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java new file mode 100644 index 00000000..cb34eeb4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java new file mode 100644 index 00000000..4aa6d80a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mFilterData; + + public LogCatFilterLabelProvider(Map filterData) { + mFilterData = filterData; + } + + @Override + public Image getColumnImage(Object arg0, int arg1) { + return null; + } + + /** + * Implements {@link ITableLabelProvider#getColumnText(Object, int)}. + * @param element an instance of {@link LogCatFilter} + * @param index index of the column + * @return text to use in the column + */ + @Override + public String getColumnText(Object element, int index) { + if (!(element instanceof LogCatFilter)) { + return null; + } + + LogCatFilter f = (LogCatFilter) element; + LogCatFilterData fd = mFilterData.get(f); + + if (fd != null && fd.getUnreadCount() > 0) { + return String.format("%s (%d)", f.getName(), fd.getUnreadCount()); + } else { + return f.getName(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java new file mode 100644 index 00000000..8b8238cc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 getLogLevels() { + List logLevels = new ArrayList(); + + for (LogLevel l : LogLevel.values()) { + logLevels.add(l.getStringValue()); + } + + return logLevels; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java new file mode 100644 index 00000000..d65e7f87 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 filters, + Map filterData) { + StringBuffer sb = new StringBuffer(); + + for (LogCatFilter f : filters) { + LogCatFilterData fd = filterData.get(f); + if (fd != null && fd.isTransient()) { + // do not persist transient filters + continue; + } + + sb.append(KW_NAME); sb.append(KW_DELIM); sb.append(quoteString(f.getName())); + sb.append(ATTR_DELIM); + sb.append(KW_TAG); sb.append(KW_DELIM); sb.append(quoteString(f.getTag())); + sb.append(ATTR_DELIM); + sb.append(KW_TEXT); sb.append(KW_DELIM); sb.append(quoteString(f.getText())); + sb.append(ATTR_DELIM); + sb.append(KW_PID); sb.append(KW_DELIM); sb.append(quoteString(f.getPid())); + sb.append(ATTR_DELIM); + sb.append(KW_APP); sb.append(KW_DELIM); sb.append(quoteString(f.getAppName())); + sb.append(ATTR_DELIM); + sb.append(KW_LOGLEVEL); sb.append(KW_DELIM); + sb.append(quoteString(f.getLogLevel().getStringValue())); + sb.append(ATTR_DELIM); + } + return sb.toString(); + } + + /** + * Decode an encoded string representing the settings of a list of logcat + * filters into a list of {@link LogCatFilter}'s. + * @param pref encoded preference string + * @return a list of {@link LogCatFilter} + */ + public List decodeFromPreferenceString(String pref) { + List fs = new ArrayList(); + + /* first split the string into a list of key, value pairs */ + List kv = getKeyValues(pref); + if (kv.size() == 0) { + return fs; + } + + /* construct filter settings from the key value pairs */ + int index = 0; + while (index < kv.size()) { + String name = ""; + String tag = ""; + String pid = ""; + String app = ""; + String text = ""; + LogLevel level = LogLevel.VERBOSE; + + assert kv.get(index).equals(KW_NAME); + name = kv.get(index + 1); + + index += 2; + while (index < kv.size() && !kv.get(index).equals(KW_NAME)) { + String key = kv.get(index); + String value = kv.get(index + 1); + index += 2; + + if (key.equals(KW_TAG)) { + tag = value; + } else if (key.equals(KW_TEXT)) { + text = value; + } else if (key.equals(KW_PID)) { + pid = value; + } else if (key.equals(KW_APP)) { + app = value; + } else if (key.equals(KW_LOGLEVEL)) { + level = LogLevel.getByString(value); + } + } + + fs.add(new LogCatFilter(name, tag, text, pid, app, level)); + } + + return fs; + } + + private List getKeyValues(String pref) { + List kv = new ArrayList(); + int index = 0; + while (index < pref.length()) { + String kw = getKeyword(pref.substring(index)); + if (kw == null) { + break; + } + index += kw.length() + KW_DELIM.length(); + + String value = getNextString(pref.substring(index)); + index += value.length() + ATTR_DELIM.length(); + + value = unquoteString(value); + + kv.add(kw); + kv.add(value); + } + + return kv; + } + + /** + * Enclose a string in quotes, escaping all the quotes within the string. + */ + private String quoteString(String s) { + return SINGLE_QUOTE + s.replace(Character.toString(SINGLE_QUOTE), "\\'") + + SINGLE_QUOTE; + } + + /** + * Recover original string from its escaped version created using + * {@link LogCatFilterSettingsSerializer#quoteString(String)}. + */ + private String unquoteString(String s) { + s = s.substring(1, s.length() - 1); /* remove start and end QUOTES */ + return s.replace("\\'", Character.toString(SINGLE_QUOTE)); + } + + private String getKeyword(String pref) { + int kwlen = pref.indexOf(KW_DELIM); + if (kwlen == -1) { + return null; + } + + return pref.substring(0, kwlen); + } + + /** + * Get the next quoted string from the input stream of characters. + */ + private String getNextString(String s) { + assert s.charAt(0) == SINGLE_QUOTE; + + StringBuffer sb = new StringBuffer(); + + int index = 0; + while (index < s.length()) { + sb.append(s.charAt(index)); + + if (index > 0 + && s.charAt(index) == SINGLE_QUOTE // current char is a single quote + && s.charAt(index - 1) != ESCAPE_CHAR) { // prev char wasn't a backslash + /* break if an unescaped SINGLE QUOTE (end of string) is seen */ + break; + } + + index++; + } + + return sb.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java new file mode 100644 index 00000000..eaaaa90c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mQ; + + /** + * Construct an empty message list. + * @param maxMessages capacity of the circular buffer + */ + public LogCatMessageList(int maxMessages) { + mFifoSize = maxMessages; + + mQ = new ArrayBlockingQueue(mFifoSize); + } + + /** + * Resize the message list. + * @param n new size for the list + */ + public synchronized void resize(int n) { + mFifoSize = n; + + if (mFifoSize > mQ.size()) { + /* if resizing to a bigger fifo, we can copy over all elements from the current mQ */ + mQ = new ArrayBlockingQueue(mFifoSize, true, mQ); + } else { + /* for a smaller fifo, copy over the last n entries */ + LogCatMessage[] curMessages = mQ.toArray(new LogCatMessage[mQ.size()]); + mQ = new ArrayBlockingQueue(mFifoSize); + for (int i = curMessages.length - mFifoSize; i < curMessages.length; i++) { + mQ.offer(curMessages[i]); + } + } + } + + /** + * Append a message to the list. If the list is full, the first + * message will be popped off of it. + * @param m log to be inserted + */ + public synchronized void appendMessages(final List messages) { + ensureSpace(messages.size()); + for (LogCatMessage m: messages) { + mQ.offer(m); + } + } + + /** + * Ensure that there is sufficient space for given number of messages. + * @return list of messages that were deleted to create additional space. + */ + public synchronized List ensureSpace(int messageCount) { + List l = new ArrayList(messageCount); + + while (mQ.remainingCapacity() < messageCount) { + l.add(mQ.poll()); + } + + return l; + } + + /** + * Returns the number of additional elements that this queue can + * ideally (in the absence of memory or resource constraints) + * accept without blocking. + * @return the remaining capacity + */ + public synchronized int remainingCapacity() { + return mQ.remainingCapacity(); + } + + /** Clear all messages in the list. */ + public synchronized void clear() { + mQ.clear(); + } + + /** Obtain a copy of the message list. */ + public synchronized List getAllMessages() { + return new ArrayList(mQ); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java new file mode 100644 index 00000000..02fa27b5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceConverter; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.FocusEvent; +import org.eclipse.swt.events.FocusListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * LogCatPanel displays a table listing the logcat messages. + */ +public final class LogCatPanel extends SelectionDependentPanel + implements ILogCatBufferChangeListener { + /** Preference key to use for storing list of logcat filters. */ + public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list"; + + /** Preference key to use for storing font settings. */ + public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; + + /** Preference key to use for deciding whether to automatically en/disable scroll lock. */ + public static final String AUTO_SCROLL_LOCK_PREFKEY = "logcat.view.auto-scroll-lock"; + + // Preference keys for message colors based on severity level + private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color."; + public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$ + public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$ + public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$ + public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$ + public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$ + public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$ + + // Use a monospace font family + private static final String FONT_FAMILY = + DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; + + // Use the default system font size + private static final FontData DEFAULT_LOGCAT_FONTDATA; + static { + int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight(); + DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL); + } + + private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize."; + private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters"; + + /** Default message to show in the message search field. */ + private static final String DEFAULT_SEARCH_MESSAGE = + "Search for messages. Accepts Java regexes. " + + "Prefix with pid:, app:, tag: or text: to limit scope."; + + /** Tooltip to show in the message search field. */ + private static final String DEFAULT_SEARCH_TOOLTIP = + "Example search patterns:\n" + + " sqlite (search for sqlite in text field)\n" + + " app:browser (search for messages generated by the browser application)"; + + private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$ + private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$ + private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$ + private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$ + private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$ + private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$ + private static final String IMAGE_SCROLL_LOCK = "scroll_lock.png"; //$NON-NLS-1$ + + private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85}; + private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100}; + + /** Index of the default filter in the saved filters column. */ + private static final int DEFAULT_FILTER_INDEX = 0; + + /* Text colors for the filter box */ + private static final Color VALID_FILTER_REGEX_COLOR = + Display.getDefault().getSystemColor(SWT.COLOR_BLACK); + private static final Color INVALID_FILTER_REGEX_COLOR = + Display.getDefault().getSystemColor(SWT.COLOR_RED); + + private LogCatReceiver mReceiver; + private IPreferenceStore mPrefStore; + + private List mLogCatFilters; + private Map mLogCatFilterData; + private int mCurrentSelectedFilterIndex; + + private ToolItem mNewFilterToolItem; + private ToolItem mDeleteFilterToolItem; + private ToolItem mEditFilterToolItem; + private TableViewer mFiltersTableViewer; + + private Combo mLiveFilterLevelCombo; + private Text mLiveFilterText; + + private List mCurrentFilters = Collections.emptyList(); + + private Table mTable; + + private boolean mShouldScrollToLatestLog = true; + private ToolItem mScrollLockCheckBox; + private boolean mAutoScrollLock; + + // Lock under which the vertical scroll bar listener should be added + private final Object mScrollBarSelectionListenerLock = new Object(); + private SelectionListener mScrollBarSelectionListener; + private boolean mScrollBarListenerSet = false; + + private String mLogFileExportFolder; + + private Font mFont; + private int mWrapWidthInChars; + + private Color mVerboseColor; + private Color mDebugColor; + private Color mInfoColor; + private Color mWarnColor; + private Color mErrorColor; + private Color mAssertColor; + + private SashForm mSash; + + // messages added since last refresh, synchronized on mLogBuffer + private List mLogBuffer; + + // # of messages deleted since last refresh, synchronized on mLogBuffer + private int mDeletedLogCount; + + private ImageFactory mImageFactory; + + /** + * Construct a logcat panel. + * @param prefStore preference store where UI preferences will be saved + */ + public LogCatPanel(IPreferenceStore prefStore, ImageFactory imageFactory) { + mPrefStore = prefStore; + mImageFactory = imageFactory; + mLogBuffer = new ArrayList(LogCatMessageList.MAX_MESSAGES_DEFAULT); + + initializeFilters(); + + setupDefaultPreferences(); + initializePreferenceUpdateListeners(); + + mFont = getFontFromPrefStore(); + loadMessageColorPreferences(); + mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); + } + + private void loadMessageColorPreferences() { + if (mVerboseColor != null) { + disposeMessageColors(); + } + + mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY); + mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY); + mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY); + mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY); + mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY); + mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY); + } + + private void initializeFilters() { + mLogCatFilters = new ArrayList(); + mLogCatFilterData = new ConcurrentHashMap(); + + /* add default filter matching all messages */ + String tag = ""; + String text = ""; + String pid = ""; + String app = ""; + LogCatFilter defaultFilter = new LogCatFilter("All messages (no filters)", + tag, text, pid, app, LogLevel.VERBOSE); + + mLogCatFilters.add(defaultFilter); + mLogCatFilterData.put(defaultFilter, new LogCatFilterData(defaultFilter)); + + /* restore saved filters from prefStore */ + List savedFilters = getSavedFilters(); + for (LogCatFilter f: savedFilters) { + mLogCatFilters.add(f); + mLogCatFilterData.put(f, new LogCatFilterData(f)); + } + } + + private void setupDefaultPreferences() { + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, + DEFAULT_LOGCAT_FONTDATA); + mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, + LogCatMessageList.MAX_MESSAGES_DEFAULT); + mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); + mPrefStore.setDefault(AUTO_SCROLL_LOCK_PREFKEY, true); + + /* Default Colors for different log levels. */ + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY, + new RGB(0, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY, + new RGB(0, 0, 127)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY, + new RGB(0, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY, + new RGB(255, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY, + new RGB(255, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY, + new RGB(255, 0, 0)); + } + + private void initializePreferenceUpdateListeners() { + mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + String changedProperty = event.getProperty(); + if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) { + if (mFont != null) { + mFont.dispose(); + } + mFont = getFontFromPrefStore(); + recomputeWrapWidth(); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + for (TableItem it: mTable.getItems()) { + it.setFont(mFont); + } + } + }); + } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) { + loadMessageColorPreferences(); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Color c = mVerboseColor; + for (TableItem it: mTable.getItems()) { + Object data = it.getData(); + if (data instanceof LogCatMessage) { + c = getForegroundColor((LogCatMessage) data); + } + it.setForeground(c); + } + } + }); + } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) { + mReceiver.resizeFifo(mPrefStore.getInt( + LogCatMessageList.MAX_MESSAGES_PREFKEY)); + reloadLogBuffer(); + } else if (changedProperty.equals(AUTO_SCROLL_LOCK_PREFKEY)) { + mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); + } + } + }); + } + + private void saveFilterPreferences() { + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + + /* save all filter settings except the first one which is the default */ + String e = serializer.encodeToPreferenceString( + mLogCatFilters.subList(1, mLogCatFilters.size()), mLogCatFilterData); + mPrefStore.setValue(LOGCAT_FILTERS_LIST, e); + } + + private List getSavedFilters() { + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String e = mPrefStore.getString(LOGCAT_FILTERS_LIST); + return serializer.decodeFromPreferenceString(e); + } + + @Override + public void deviceSelected() { + IDevice device = getCurrentDevice(); + if (device == null) { + // If the device is not working properly, getCurrentDevice() could return null. + // In such a case, we don't launch logcat, nor switch the display. + return; + } + + if (mReceiver != null) { + // Don't need to listen to new logcat messages from previous device anymore. + mReceiver.removeMessageReceivedEventListener(this); + + // When switching between devices, existing filter match count should be reset. + for (LogCatFilter f : mLogCatFilters) { + LogCatFilterData fd = mLogCatFilterData.get(f); + fd.resetUnreadCount(); + } + } + + mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore); + mReceiver.addMessageReceivedEventListener(this); + reloadLogBuffer(); + + // Always scroll to last line whenever the selected device changes. + // Run this in a separate async thread to give the table some time to update after the + // setInput above. + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + scrollToLatestLog(); + } + }); + } + + @Override + public void clientSelected() { + } + + @Override + protected void postCreation() { + } + + @Override + protected Control createControl(Composite parent) { + GridLayout layout = new GridLayout(1, false); + parent.setLayout(layout); + + createViews(parent); + setupDefaults(); + + return null; + } + + private void createViews(Composite parent) { + mSash = createSash(parent); + + createListOfFilters(mSash); + createLogTableView(mSash); + + boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY); + updateFiltersColumn(showFilters); + } + + private SashForm createSash(Composite parent) { + SashForm sash = new SashForm(parent, SWT.HORIZONTAL); + sash.setLayoutData(new GridData(GridData.FILL_BOTH)); + return sash; + } + + private void createListOfFilters(SashForm sash) { + Composite c = new Composite(sash, SWT.BORDER); + GridLayout layout = new GridLayout(2, false); + c.setLayout(layout); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createFiltersToolbar(c); + createFiltersTable(c); + } + + private void createFiltersToolbar(Composite parent) { + Label l = new Label(parent, SWT.NONE); + l.setText("Saved Filters"); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.LEFT; + l.setLayoutData(gd); + + ToolBar t = new ToolBar(parent, SWT.FLAT); + gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + t.setLayoutData(gd); + + /* new filter */ + mNewFilterToolItem = new ToolItem(t, SWT.PUSH); + mNewFilterToolItem.setImage( + mImageFactory.getImageByName(IMAGE_ADD_FILTER)); + 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( + mImageFactory.getImageByName(IMAGE_DELETE_FILTER)); + 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( + mImageFactory.getImageByName(IMAGE_EDIT_FILTER)); + 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(mImageFactory.getImageByName(IMAGE_SAVE_LOG_TO_FILE)); + 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( + mImageFactory.getImageByName(IMAGE_CLEAR_LOG)); + 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( + mImageFactory.getImageByName(IMAGE_DISPLAY_FILTERS)); + 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( + mImageFactory.getImageByName(IMAGE_SCROLL_LOCK)); + mScrollLockCheckBox.setSelection(true); + mScrollLockCheckBox.setToolTipText("Scroll Lock"); + mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + boolean scrollLock = mScrollLockCheckBox.getSelection(); + setScrollToLatestLog(scrollLock); + } + }); + } + + /** Sets the foreground color of filter text based on whether the regex is valid. */ + private void updateFilterTextColor() { + String text = mLiveFilterText.getText(); + Color c; + try { + Pattern.compile(text.trim()); + c = VALID_FILTER_REGEX_COLOR; + } catch (PatternSyntaxException e) { + c = INVALID_FILTER_REGEX_COLOR; + } + mLiveFilterText.setForeground(c); + } + + private void updateFiltersColumn(boolean showFilters) { + if (showFilters) { + mSash.setWeights(WEIGHTS_SHOW_FILTERS); + } else { + mSash.setWeights(WEIGHTS_LOGCAT_ONLY); + } + } + + /** + * Save logcat messages selected in the table to a file. + */ + private void saveLogToFile() { + /* show dialog box and get target file name */ + final String fName = getLogFileTargetLocation(); + if (fName == null) { + return; + } + + /* obtain list of selected messages */ + final List selectedMessages = getSelectedLogCatMessages(); + + /* save messages to file in a different (non UI) thread */ + Thread t = new Thread(new Runnable() { + @Override + public void run() { + BufferedWriter w = null; + try { + w = new BufferedWriter(new FileWriter(fName)); + for (LogCatMessage m : selectedMessages) { + w.append(m.toString()); + w.newLine(); + } + } catch (final IOException e) { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + MessageDialog.openError(Display.getCurrent().getActiveShell(), + "Unable to export selection to file.", + "Unexpected error while saving selected messages to file: " + + e.getMessage()); + } + }); + } finally { + if (w != null) { + try { + w.close(); + } catch (IOException e) { + // ignore + } + } + } + } + }); + t.setName("Saving selected items to logfile.."); + t.start(); + } + + /** + * Display a {@link FileDialog} to the user and obtain the location for the log file. + * @return path to target file, null if user canceled the dialog + */ + private String getLogFileTargetLocation() { + FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); + + fd.setText("Save Log.."); + fd.setFileName("log.txt"); + + if (mLogFileExportFolder == null) { + mLogFileExportFolder = System.getProperty("user.home"); + } + fd.setFilterPath(mLogFileExportFolder); + + fd.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + fd.setFilterExtensions(new String[] { + "*.txt" + }); + + String fName = fd.open(); + if (fName != null) { + mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */ + } + + return fName; + } + + private List getSelectedLogCatMessages() { + int[] indices = mTable.getSelectionIndices(); + Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ + + List selectedMessages = new ArrayList(indices.length); + for (int i : indices) { + Object data = mTable.getItem(i).getData(); + if (data instanceof LogCatMessage) { + selectedMessages.add((LogCatMessage) data); + } + } + + return selectedMessages; + } + + private List applyCurrentFilters(List msgList) { + List filteredItems = new ArrayList(msgList.size()); + + for (LogCatMessage msg: msgList) { + if (isMessageAccepted(msg, mCurrentFilters)) { + filteredItems.add(msg); + } + } + + return filteredItems; + } + + private boolean isMessageAccepted(LogCatMessage msg, List filters) { + for (LogCatFilter f : filters) { + if (!f.matches(msg)) { + // not accepted by this filter + return false; + } + } + + // accepted by all filters + return true; + } + + private void createLogcatViewTable(Composite parent) { + mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI); + + mTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mTable.getHorizontalBar().setVisible(true); + + /** Columns to show in the table. */ + String[] properties = { + "Level", + "Time", + "PID", + "TID", + "Application", + "Tag", + "Text", + }; + + /** The sampleText for each column is used to determine the default widths + * for each column. The contents do not matter, only their lengths are needed. */ + String[] sampleText = { + " ", + " 00-00 00:00:00.0000 ", + " 0000", + " 0000", + " com.android.launcher", + " SampleTagText", + " Log Message field should be pretty long by default. As long as possible for correct display on Mac.", + }; + + for (int i = 0; i < properties.length; i++) { + TableHelper.createTableColumn(mTable, + properties[i], /* Column title */ + SWT.LEFT, /* Column Style */ + sampleText[i], /* String to compute default col width */ + getColPreferenceKey(properties[i]), /* Preference Store key for this column */ + mPrefStore); + } + + // don't zebra stripe the table: When the buffer is full, and scroll lock is on, having + // zebra striping means that the background could keep changing depending on the number + // of new messages added to the bottom of the log. + mTable.setLinesVisible(false); + mTable.setHeaderVisible(true); + + // Set the row height to be sufficient enough to display the current font. + // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So + // we explicitly set it to be sure. + mTable.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + event.height = event.gc.getFontMetrics().getHeight(); + } + }); + + // Update the label provider whenever the text column's width changes + TableColumn textColumn = mTable.getColumn(properties.length - 1); + textColumn.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent event) { + recomputeWrapWidth(); + } + }); + + addRightClickMenu(mTable); + initDoubleClickListener(); + recomputeWrapWidth(); + + mTable.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent arg0) { + dispose(); + } + }); + + final ScrollBar vbar = mTable.getVerticalBar(); + mScrollBarSelectionListener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (!mAutoScrollLock) { + return; + } + + // thumb + selection < max => bar is not at the bottom. + // We subtract an arbitrary amount (thumbSize/2) from this difference to allow + // for cases like half a line being displayed at the end from affecting this + // calculation. The thumbSize/2 number seems to work experimentally across + // Linux/Mac & Windows, but might possibly need tweaking. + int diff = vbar.getThumb() + vbar.getSelection() - vbar.getMaximum(); + boolean isAtBottom = Math.abs(diff) < vbar.getThumb() / 2; + + if (isAtBottom != mShouldScrollToLatestLog) { + setScrollToLatestLog(isAtBottom); + mScrollLockCheckBox.setSelection(isAtBottom); + } + } + }; + startScrollBarMonitor(vbar); + + // Explicitly set the values to use for the scroll bar. In particular, we want these values + // to have a high enough accuracy that even small movements of the scroll bar have an + // effect on the selection. The auto scroll lock detection assumes that the scroll bar is + // at the bottom iff selection + thumb == max. + final int MAX = 10000; + final int THUMB = 10; + vbar.setValues(MAX - THUMB, // selection + 0, // min + MAX, // max + THUMB, // thumb + 1, // increment + THUMB); // page increment + } + + private void startScrollBarMonitor(ScrollBar vbar) { + synchronized (mScrollBarSelectionListenerLock) { + if (!mScrollBarListenerSet) { + mScrollBarListenerSet = true; + vbar.addSelectionListener(mScrollBarSelectionListener); + } + } + } + + private void stopScrollBarMonitor(ScrollBar vbar) { + synchronized (mScrollBarSelectionListenerLock) { + if (mScrollBarListenerSet) { + mScrollBarListenerSet = false; + vbar.removeSelectionListener(mScrollBarSelectionListener); + } + } + } + + /** Setup menu to be displayed when right clicking a log message. */ + private void addRightClickMenu(final Table table) { + // This action will pop up a create filter dialog pre-populated with current selection + final Action filterAction = new Action("Filter similar messages...") { + @Override + public void run() { + List selectedMessages = getSelectedLogCatMessages(); + if (selectedMessages.size() == 0) { + addNewFilter(); + } else { + LogCatMessage m = selectedMessages.get(0); + addNewFilter(m.getTag(), m.getMessage(), Integer.toString(m.getPid()), m.getAppName(), + m.getLogLevel()); + } + } + }; + + final Action findAction = new Action("Find...") { + @Override + public void run() { + showFindDialog(); + }; + }; + + final MenuManager mgr = new MenuManager(); + mgr.add(filterAction); + mgr.add(findAction); + final Menu menu = mgr.createContextMenu(table); + + table.addListener(SWT.MenuDetect, new Listener() { + @Override + public void handleEvent(Event event) { + Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y)); + Rectangle clientArea = table.getClientArea(); + + // The click location is in the header if it is between + // clientArea.y and clientArea.y + header height + boolean header = pt.y > clientArea.y + && pt.y < (clientArea.y + table.getHeaderHeight()); + + // Show the menu only if it is not inside the header + table.setMenu(header ? null : menu); + } + }); + } + + public void recomputeWrapWidth() { + if (mTable == null || mTable.isDisposed()) { + return; + } + + // get width of the last column (log message) + TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1); + int colWidth = tc.getWidth(); + + // get font width + GC gc = new GC(tc.getParent()); + gc.setFont(mFont); + int avgCharWidth = gc.getFontMetrics().getAverageCharWidth(); + gc.dispose(); + + int MIN_CHARS_PER_LINE = 50; // show atleast these many chars per line + mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE); + + int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line + mWrapWidthInChars -= OFFSET_AT_END_OF_LINE; + } + + private void setScrollToLatestLog(boolean scroll) { + mShouldScrollToLatestLog = scroll; + if (scroll) { + scrollToLatestLog(); + } + } + + private String getColPreferenceKey(String field) { + return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field; + } + + private Font getFontFromPrefStore() { + FontData fd = PreferenceConverter.getFontData(mPrefStore, + LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY); + return new Font(Display.getDefault(), fd); + } + + private Color getColorFromPrefStore(String key) { + RGB rgb = PreferenceConverter.getColor(mPrefStore, key); + return new Color(Display.getDefault(), rgb); + } + + private void setupDefaults() { + int defaultFilterIndex = 0; + mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); + + filterSelectionChanged(); + } + + /** + * Perform all necessary updates whenever a filter is selected (by user or programmatically). + */ + private void filterSelectionChanged() { + int idx = mFiltersTableViewer.getTable().getSelectionIndex(); + if (idx == -1) { + /* One of the filters should always be selected. + * On Linux, there is no way to deselect an item. + * On Mac, clicking inside the list view, but not an any item will result + * in all items being deselected. In such a case, we simply reselect the + * first entry. */ + idx = 0; + mFiltersTableViewer.getTable().setSelection(idx); + } + + mCurrentSelectedFilterIndex = idx; + + resetUnreadCountForAllFilters(); + updateFiltersToolBar(); + updateAppliedFilters(); + } + + private void resetUnreadCountForAllFilters() { + for (LogCatFilterData fd: mLogCatFilterData.values()) { + fd.resetUnreadCount(); + } + refreshFiltersTable(); + } + + private void updateFiltersToolBar() { + /* The default filter at index 0 can neither be edited, nor removed. */ + boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX; + mEditFilterToolItem.setEnabled(en); + mDeleteFilterToolItem.setEnabled(en); + } + + private void updateAppliedFilters() { + mCurrentFilters = getFiltersToApply(); + reloadLogBuffer(); + } + + private List getFiltersToApply() { + /* list of filters to apply = saved filter + live filters */ + List filters = new ArrayList(); + + if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) { + filters.add(getSelectedSavedFilter()); + } + + filters.addAll(getCurrentLiveFilters()); + return filters; + } + + private List getCurrentLiveFilters() { + return LogCatFilter.fromString( + mLiveFilterText.getText(), /* current query */ + LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */ + } + + private LogCatFilter getSelectedSavedFilter() { + return mLogCatFilters.get(mCurrentSelectedFilterIndex); + } + + @Override + public void setFocus() { + } + + @Override + public void bufferChanged(List addedMessages, + List deletedMessages) { + updateUnreadCount(addedMessages); + refreshFiltersTable(); + + synchronized (mLogBuffer) { + addedMessages = applyCurrentFilters(addedMessages); + deletedMessages = applyCurrentFilters(deletedMessages); + + mLogBuffer.addAll(addedMessages); + mDeletedLogCount += deletedMessages.size(); + } + + refreshLogCatTable(); + } + + private void reloadLogBuffer() { + mTable.removeAll(); + + synchronized (mLogBuffer) { + mLogBuffer.clear(); + mDeletedLogCount = 0; + } + + if (mReceiver == null || mReceiver.getMessages() == null) { + return; + } + + List addedMessages = mReceiver.getMessages().getAllMessages(); + List deletedMessages = Collections.emptyList(); + bufferChanged(addedMessages, deletedMessages); + } + + /** + * When new messages are received, and they match a saved filter, update + * the unread count associated with that filter. + * @param receivedMessages list of new messages received + */ + private void updateUnreadCount(List receivedMessages) { + for (int i = 0; i < mLogCatFilters.size(); i++) { + if (i == mCurrentSelectedFilterIndex) { + /* no need to update unread count for currently selected filter */ + continue; + } + LogCatFilter f = mLogCatFilters.get(i); + LogCatFilterData fd = mLogCatFilterData.get(f); + fd.updateUnreadCount(receivedMessages); + } + } + + private void refreshFiltersTable() { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + if (mFiltersTableViewer.getTable().isDisposed()) { + return; + } + mFiltersTableViewer.refresh(); + } + }); + } + + /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */ + private LogCatTableRefresherTask mCurrentRefresher; + + /** + * Refresh the logcat table asynchronously from the UI thread. + * This method adds a new async refresh only if there are no pending refreshes for the table. + * Doing so eliminates redundant refresh threads from being queued up to be run on the + * display thread. + */ + private void refreshLogCatTable() { + synchronized (this) { + if (mCurrentRefresher == null) { + mCurrentRefresher = new LogCatTableRefresherTask(); + Display.getDefault().asyncExec(mCurrentRefresher); + } + } + } + + /** + * The {@link LogCatTableRefresherTask} takes care of refreshing the table with the + * new log messages that have been received. Since the log behaves like a circular buffer, + * the first step is to remove items from the top of the table (if necessary). This step + * is complicated by the fact that a single log message may span multiple rows if the message + * was wrapped. Once the deleted items are removed, the new messages are added to the bottom + * of the table. If scroll lock is enabled, the item that was original visible is made visible + * again, if not, the last item is made visible. + */ + private class LogCatTableRefresherTask implements Runnable { + @Override + public void run() { + if (mTable.isDisposed()) { + return; + } + synchronized (LogCatPanel.this) { + mCurrentRefresher = null; + } + + // Current topIndex so that it can be restored if scroll locked. + int topIndex = mTable.getTopIndex(); + + mTable.setRedraw(false); + + // the scroll bar should only listen to user generated scroll events, not the + // scroll events that happen due to the addition of logs + stopScrollBarMonitor(mTable.getVerticalBar()); + + // Obtain the list of new messages, and the number of deleted messages. + List newMessages; + int deletedMessageCount; + synchronized (mLogBuffer) { + newMessages = new ArrayList(mLogBuffer); + mLogBuffer.clear(); + + deletedMessageCount = mDeletedLogCount; + mDeletedLogCount = 0; + + mFindTarget.scrollBy(deletedMessageCount); + } + + int originalItemCount = mTable.getItemCount(); + + // Remove entries from the start of the table if they were removed in the log buffer + // This is complicated by the fact that a single message may span multiple TableItems + // if it was word-wrapped. + deletedMessageCount -= removeFromTable(mTable, deletedMessageCount); + + // Compute number of table items that were deleted from the table. + int deletedItemCount = originalItemCount - mTable.getItemCount(); + + // If there are more messages to delete (after deleting messages from the table), + // then delete them from the start of the newly added messages list + if (deletedMessageCount > 0) { + assert deletedMessageCount < newMessages.size(); + for (int i = 0; i < deletedMessageCount; i++) { + newMessages.remove(0); + } + } + + // Add the remaining messages to the table. + for (LogCatMessage m: newMessages) { + List wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars); + Color c = getForegroundColor(m); + for (int i = 0; i < wrappedMessageList.size(); i++) { + TableItem item = new TableItem(mTable, SWT.NONE); + + if (i == 0) { + // Only set the message data in the first item. This allows code that + // examines the table item data (such as copy selection) to distinguish + // between real messages versus lines that are really just wrapped + // content from the previous message. + item.setData(m); + + item.setText(new String[] { + Character.toString(m.getLogLevel().getPriorityLetter()), + m.getTimestamp().toString(), + Integer.toString(m.getPid()), + Integer.toString(m.getTid()), + m.getAppName(), + m.getTag(), + wrappedMessageList.get(i) + }); + } else { + item.setText(new String[] { + "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + wrappedMessageList.get(i) + }); + } + item.setForeground(c); + item.setFont(mFont); + } + } + + if (mShouldScrollToLatestLog) { + scrollToLatestLog(); + } else { + // If scroll locked, show the same item that was original visible in the table. + int index = Math.max(topIndex - deletedItemCount, 0); + mTable.setTopIndex(index); + } + + mTable.setRedraw(true); + + // re-enable listening to scroll bar events, but do so in a separate thread to make + // sure that the current task (LogCatRefresherTask) has completed first + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + if (!mTable.isDisposed()) { + startScrollBarMonitor(mTable.getVerticalBar()); + } + } + }); + } + + /** + * Removes given number of messages from the table, starting at the top of the table. + * Note that the number of messages deleted is not equal to the number of rows + * deleted since a single message could span multiple rows. This method first calculates + * the number of rows that correspond to the number of messages to delete, and then + * removes all those rows. + * @param table table from which messages should be removed + * @param msgCount number of messages to be removed + * @return number of messages that were actually removed + */ + private int removeFromTable(Table table, int msgCount) { + int deletedMessageCount = 0; // # of messages that have been deleted + int lastItemToDelete = 0; // index of the last item that should be deleted + + while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) { + // only rows that begin a message have their item data set + TableItem item = table.getItem(lastItemToDelete); + if (item.getData() != null) { + deletedMessageCount++; + } + + lastItemToDelete++; + } + + // If there are any table items left over at the end that are wrapped over from the + // previous message, mark them for deletion as well. + if (lastItemToDelete < table.getItemCount() + && table.getItem(lastItemToDelete).getData() == null) { + lastItemToDelete++; + } + + table.remove(0, lastItemToDelete - 1); + + return deletedMessageCount; + } + } + + /** Scroll to the last line. */ + private void scrollToLatestLog() { + if (!mTable.isDisposed()) { + mTable.setTopIndex(mTable.getItemCount() - 1); + } + } + + /** + * Splits the message into multiple lines if the message length exceeds given width. + * If the message was split, then a wrap character \u23ce is appended to the end of all + * lines but the last one. + */ + private List wrapMessage(String msg, int wrapWidth) { + if (msg.length() < wrapWidth) { + return Collections.singletonList(msg); + } + + List wrappedMessages = new ArrayList(); + + int offset = 0; + int len = msg.length(); + + while (len > 0) { + int copylen = Math.min(wrapWidth, len); + String s = msg.substring(offset, offset + copylen); + + offset += copylen; + len -= copylen; + + if (len > 0) { // if there are more lines following, then append a wrap marker + s += " \u23ce"; //$NON-NLS-1$ + } + + wrappedMessages.add(s); + } + + return wrappedMessages; + } + + private Color getForegroundColor(LogCatMessage m) { + LogLevel l = m.getLogLevel(); + + if (l.equals(LogLevel.VERBOSE)) { + return mVerboseColor; + } else if (l.equals(LogLevel.INFO)) { + return mInfoColor; + } else if (l.equals(LogLevel.DEBUG)) { + return mDebugColor; + } else if (l.equals(LogLevel.ERROR)) { + return mErrorColor; + } else if (l.equals(LogLevel.WARN)) { + return mWarnColor; + } else if (l.equals(LogLevel.ASSERT)) { + return mAssertColor; + } + + return mVerboseColor; + } + + private List mMessageSelectionListeners; + + private void initDoubleClickListener() { + mMessageSelectionListeners = new ArrayList(1); + + mTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + List selectedMessages = getSelectedLogCatMessages(); + if (selectedMessages.size() == 0) { + return; + } + + for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) { + l.messageDoubleClicked(selectedMessages.get(0)); + } + } + }); + } + + public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) { + mMessageSelectionListeners.add(l); + } + + private ITableFocusListener mTableFocusListener; + + /** + * Specify the listener to be called when the logcat view gets focus. This interface is + * required by DDMS to hook up the menu items for Copy and Select All. + * @param listener listener to be notified when logcat view is in focus + */ + public void setTableFocusListener(ITableFocusListener listener) { + mTableFocusListener = listener; + + final IFocusedTableActivator activator = new IFocusedTableActivator() { + @Override + public void copy(Clipboard clipboard) { + copySelectionToClipboard(clipboard); + } + + @Override + public void selectAll() { + mTable.selectAll(); + } + }; + + mTable.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + mTableFocusListener.focusGained(activator); + } + + @Override + public void focusLost(FocusEvent e) { + mTableFocusListener.focusLost(activator); + } + }); + } + + /** Copy all selected messages to clipboard. */ + public void copySelectionToClipboard(Clipboard clipboard) { + StringBuilder sb = new StringBuilder(); + + for (LogCatMessage m : getSelectedLogCatMessages()) { + sb.append(m.toString()); + sb.append('\n'); + } + + if (sb.length() > 0) { + clipboard.setContents( + new Object[] {sb.toString()}, + new Transfer[] {TextTransfer.getInstance()} + ); + } + } + + /** Select all items in the logcat table. */ + public void selectAll() { + mTable.selectAll(); + } + + private void dispose() { + if (mFont != null && !mFont.isDisposed()) { + mFont.dispose(); + } + + if (mVerboseColor != null && !mVerboseColor.isDisposed()) { + disposeMessageColors(); + } + } + + private void disposeMessageColors() { + mVerboseColor.dispose(); + mDebugColor.dispose(); + mInfoColor.dispose(); + mWarnColor.dispose(); + mErrorColor.dispose(); + mAssertColor.dispose(); + } + + private class LogcatFindTarget extends AbstractBufferFindTarget { + @Override + public void selectAndReveal(int index) { + mTable.deselectAll(); + mTable.select(index); + mTable.showSelection(); + } + + @Override + public int getItemCount() { + return mTable.getItemCount(); + } + + @Override + public String getItem(int index) { + Object data = mTable.getItem(index).getData(); + if (data != null) { + return data.toString(); + } + + return null; + } + + @Override + public int getStartingIndex() { + // start searches from current selection if present, otherwise from the tail end + // of the buffer + int s = mTable.getSelectionIndex(); + if (s != -1) { + return s; + } else { + return getItemCount() - 1; + } + }; + }; + + private FindDialog mFindDialog; + private LogcatFindTarget mFindTarget = new LogcatFindTarget(); + public void showFindDialog() { + if (mFindDialog != null) { + // if the dialog is already displayed + return; + } + + mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget); + mFindDialog.open(); // blocks until find dialog is closed + mFindDialog = null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java new file mode 100644 index 00000000..2aceafc4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.logcat.LogCatListener; +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmlib.logcat.LogCatReceiverTask; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A class to monitor a device for logcat messages. It stores the received + * log messages in a circular buffer. + */ +public final class LogCatReceiver implements LogCatListener { + private static LogCatMessage DEVICE_DISCONNECTED_MESSAGE = + new LogCatMessage(LogLevel.ERROR, "Device disconnected"); + + private LogCatMessageList mLogMessages; + private IDevice mCurrentDevice; + private LogCatReceiverTask mLogCatReceiverTask; + private Set mLogCatMessageListeners; + private IPreferenceStore mPrefStore; + + /** + * Construct a LogCat message receiver for provided device. This will launch a + * logcat command on the device, and monitor the output of that command in + * a separate thread. All logcat messages are then stored in a circular + * buffer, which can be retrieved using {@link LogCatReceiver#getMessages()}. + * @param device device to monitor for logcat messages + * @param prefStore + */ + public LogCatReceiver(IDevice device, IPreferenceStore prefStore) { + mCurrentDevice = device; + mPrefStore = prefStore; + + mLogCatMessageListeners = new HashSet(); + mLogMessages = new LogCatMessageList(getFifoSize()); + + startReceiverThread(); + } + + /** + * Stop receiving messages from currently active device. + */ + public void stop() { + if (mLogCatReceiverTask != null) { + /* stop the current logcat command */ + mLogCatReceiverTask.removeLogCatListener(this); + mLogCatReceiverTask.stop(); + mLogCatReceiverTask = null; + + // add a message to the log indicating that the device has been disconnected. + log(Collections.singletonList(DEVICE_DISCONNECTED_MESSAGE)); + } + + mCurrentDevice = null; + } + + private int getFifoSize() { + int n = mPrefStore.getInt(LogCatMessageList.MAX_MESSAGES_PREFKEY); + return n == 0 ? LogCatMessageList.MAX_MESSAGES_DEFAULT : n; + } + + private void startReceiverThread() { + if (mCurrentDevice == null) { + return; + } + + mLogCatReceiverTask = new LogCatReceiverTask(mCurrentDevice); + mLogCatReceiverTask.addLogCatListener(this); + + Thread t = new Thread(mLogCatReceiverTask); + t.setName("LogCat output receiver for " + mCurrentDevice.getSerialNumber()); + t.start(); + } + + @Override + public void log(List newMessages) { + List deletedMessages; + synchronized (mLogMessages) { + deletedMessages = mLogMessages.ensureSpace(newMessages.size()); + mLogMessages.appendMessages(newMessages); + } + sendLogChangedEvent(newMessages, deletedMessages); + } + + /** + * Get the list of logcat messages received from currently active device. + * @return list of messages if currently listening, null otherwise + */ + public LogCatMessageList getMessages() { + return mLogMessages; + } + + /** + * Clear the list of messages received from the currently active device. + */ + public void clearMessages() { + mLogMessages.clear(); + } + + /** + * Add to list of message event listeners. + * @param l listener to notified when messages are received from the device + */ + public void addMessageReceivedEventListener(ILogCatBufferChangeListener l) { + mLogCatMessageListeners.add(l); + } + + public void removeMessageReceivedEventListener(ILogCatBufferChangeListener l) { + mLogCatMessageListeners.remove(l); + } + + private void sendLogChangedEvent(List addedMessages, + List deletedMessages) { + for (ILogCatBufferChangeListener l : mLogCatMessageListeners) { + l.bufferChanged(addedMessages, deletedMessages); + } + } + + /** + * Resize the internal FIFO. + * @param size new size + */ + public void resizeFifo(int size) { + mLogMessages.resize(size); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java new file mode 100644 index 00000000..bf440eaa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mReceiverCache = new HashMap(); + + /** Private constructor: cannot instantiate. */ + private LogCatReceiverFactory() { + AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() { + @Override + public void deviceDisconnected(final IDevice device) { + // The deviceDisconnected() is called from DDMS code that holds + // multiple locks regarding list of clients, etc. + // It so happens that #newReceiver() below adds a clientChangeListener + // which requires those locks as well. So if we call + // #removeReceiverFor from a DDMS/Monitor thread, we could end up + // in a deadlock. As a result, we spawn a separate thread that + // doesn't hold any of the DDMS locks to remove the receiver. + Thread t = new Thread(new Runnable() { + @Override + public void run() { + removeReceiverFor(device); } + }, "Remove logcat receiver for " + device.getSerialNumber()); + t.start(); + } + + @Override + public void deviceConnected(IDevice device) { + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + } + }); + } + + /** + * Remove existing logcat receivers. This method should not be called from a DDMS thread + * context that might be holding locks. Doing so could result in a deadlock with the following + * two threads locked up:

    + *
  • {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory}, + * while holding a DDMS monitor internal lock.
  • + *
  • {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory} + * while attempting to obtain a DDMS monitor lock.
  • + *
+ */ + private synchronized void removeReceiverFor(IDevice device) { + LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); + if (r != null) { + r.stop(); + mReceiverCache.remove(device.getSerialNumber()); + } + } + + public synchronized LogCatReceiver newReceiver(IDevice device, IPreferenceStore prefs) { + LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); + if (r != null) { + return r; + } + + r = new LogCatReceiver(device, prefs); + mReceiverCache.put(device.getSerialNumber(), r); + return r; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java new file mode 100644 index 00000000..f37484f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 00000000..56bd94c4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 00000000..6b15ad1f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mMessages = new ArrayList(); + private final ArrayList mNewMessages = new ArrayList(); + + private boolean mSupportsDelete = true; + private boolean mSupportsEdit = true; + private int mRemovedMessageCount = 0; + + /** + * Creates a filter with a particular mode. + * @param name The name to be displayed in the UI + */ + public LogFilter(String name) { + mName = name; + } + + public LogFilter() { + + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mName); + + sb.append(':'); + sb.append(mMode); + if ((mMode & MODE_PID) == MODE_PID) { + sb.append(':'); + sb.append(mPid); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + sb.append(':'); + sb.append(mLogLevel); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + sb.append(':'); + sb.append(mTag); + } + + return sb.toString(); + } + + public boolean loadFromString(String string) { + String[] segments = string.split(":"); //$NON-NLS-1$ + int index = 0; + + // get the name + mName = segments[index++]; + + // get the mode + mMode = Integer.parseInt(segments[index++]); + + if ((mMode & MODE_PID) == MODE_PID) { + mPid = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + mLogLevel = Integer.parseInt(segments[index++]); + } + + if ((mMode & MODE_TAG) == MODE_TAG) { + mTag = segments[index++]; + } + + return true; + } + + + /** Sets the name of the filter. */ + void setName(String name) { + mName = name; + } + + /** + * Returns the UI display name. + */ + public String getName() { + return mName; + } + + /** + * Set the Table ui widget associated with this filter. + * @param tabItem The item in the TabFolder + * @param table The Table object + */ + public void setWidgets(TabItem tabItem, Table table) { + mTable = table; + mTabItem = tabItem; + } + + /** + * Returns true if the filter is ready for ui. + */ + public boolean uiReady() { + return (mTable != null && mTabItem != null); + } + + /** + * Returns the UI table object. + * @return + */ + public Table getTable() { + return mTable; + } + + public void dispose() { + mTable.dispose(); + mTabItem.dispose(); + mTable = null; + mTabItem = null; + } + + /** + * Resets the filtering mode to be 0 (i.e. no filter). + */ + public void resetFilteringMode() { + mMode = 0; + } + + /** + * Returns the current filtering mode. + * @return A bitmask. Possible values are MODE_PID, MODE_TAG, MODE_LEVEL + */ + public int getFilteringMode() { + return mMode; + } + + /** + * Adds PID to the current filtering mode. + * @param pid + */ + public void setPidMode(int pid) { + if (pid != -1) { + mMode |= MODE_PID; + } else { + mMode &= ~MODE_PID; + } + mPid = pid; + } + + /** Returns the pid filter if valid, otherwise -1 */ + public int getPidFilter() { + if ((mMode & MODE_PID) == MODE_PID) + return mPid; + return -1; + } + + public void setTagMode(String tag) { + if (tag != null && tag.length() > 0) { + mMode |= MODE_TAG; + } else { + mMode &= ~MODE_TAG; + } + mTag = tag; + } + + public String getTagFilter() { + if ((mMode & MODE_TAG) == MODE_TAG) + return mTag; + return null; + } + + public void setLogLevel(int level) { + if (level == -1) { + mMode &= ~MODE_LEVEL; + } else { + mMode |= MODE_LEVEL; + mLogLevel = level; + } + + } + + public int getLogLevel() { + if ((mMode & MODE_LEVEL) == MODE_LEVEL) { + return mLogLevel; + } + + return -1; + } + + + public boolean supportsDelete() { + return mSupportsDelete ; + } + + public boolean supportsEdit() { + return mSupportsEdit; + } + + /** + * Sets the selected state of the filter. + * @param selected selection state. + */ + public void setSelectedState(boolean selected) { + if (selected) { + if (mTabItem != null) { + mTabItem.setText(mName); + } + mUnreadCount = 0; + } + mIsCurrentTabItem = selected; + } + + /** + * Adds a new message and optionally removes an old message. + *

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

This adds the new message to the buffer, and gives it to the existing filters. + * @param newMessage + */ + private void processNewMessage(LogMessage newMessage) { + // if we are in auto filtering mode, make sure we have + // a filter for this + if (mFilterMode == FILTER_AUTO_PID || + mFilterMode == FILTER_AUTO_TAG) { + checkFilter(newMessage.data); + } + + // compute the index where the message goes. + // was the buffer empty? + int messageIndex = -1; + if (mBufferStart == -1) { + messageIndex = mBufferStart = 0; + mBufferEnd = 1; + } else { + messageIndex = mBufferEnd; + + // increment the next usable slot index + mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH; + + // check we aren't overwriting start + if (mBufferEnd == mBufferStart) { + mBufferStart = (mBufferStart + 1) % STRING_BUFFER_LENGTH; + } + } + + LogMessage oldMessage = null; + + // record the message that was there before + if (mBuffer[messageIndex] != null) { + oldMessage = mBuffer[messageIndex]; + } + + // then add the new one + mBuffer[messageIndex] = newMessage; + + // give the new message to every filters. + boolean filtered = false; + if (mFilters != null) { + for (LogFilter f : mFilters) { + filtered |= f.addMessage(newMessage, oldMessage); + } + } + if (filtered == false && mDefaultFilter != null) { + mDefaultFilter.addMessage(newMessage, oldMessage); + } + } + + private void createFilters() { + if (mFilterMode == FILTER_DEBUG || mFilterMode == FILTER_MANUAL) { + // unarchive the filters. + mFilters = mFilterStorage.getFilterFromStore(); + + // set the colors + if (mFilters != null) { + for (LogFilter f : mFilters) { + f.setColors(mColors); + } + } + + if (mFilterStorage.requiresDefaultFilter()) { + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } else if (mFilterMode == FILTER_NONE) { + // if the filtering mode is "none", we create a single filter that + // will receive all + mDefaultFilter = new LogFilter("Log"); + mDefaultFilter.setColors(mColors); + mDefaultFilter.setSupportsDelete(false); + mDefaultFilter.setSupportsEdit(false); + } + } + + /** Checks if there's an automatic filter for this md and if not + * adds the filter and the ui. + * This must be called from the UI! + * @param md + * @return true if the filter existed already + */ + private boolean checkFilter(final LogMessageInfo md) { + if (true) + return true; + // look for a filter that matches the pid + if (mFilterMode == FILTER_AUTO_PID) { + for (LogFilter f : mFilters) { + if (f.getPidFilter() == md.pid) { + return true; + } + } + } else if (mFilterMode == FILTER_AUTO_TAG) { + for (LogFilter f : mFilters) { + if (f.getTagFilter().equals(md.tag)) { + return true; + } + } + } + + // if we reach this point, no filter was found. + // create a filter with a temporary name of the pid + final LogFilter newFilter = new LogFilter(md.pidString); + String name = null; + if (mFilterMode == FILTER_AUTO_PID) { + newFilter.setPidMode(md.pid); + + // ask the monitor thread if it knows the pid. + name = mCurrentLoggedDevice.getClientName(md.pid); + } else { + newFilter.setTagMode(md.tag); + name = md.tag; + } + addFilterToArray(newFilter); + + final String fname = name; + + // create the tabitem + final TabItem newTabItem = createTab(newFilter, -1, true); + + // if the name is unknown + if (fname == null) { + // we need to find the process running under that pid. + // launch a thread do a ps on the device + new Thread("remote PS") { //$NON-NLS-1$ + @Override + public void run() { + // create the receiver + PsOutputReceiver psor = new PsOutputReceiver(md.pid, + newFilter, newTabItem); + + // execute ps + try { + mCurrentLoggedDevice.executeShellCommand("ps", psor); //$NON-NLS-1$ + } catch (IOException e) { + // Ignore + } catch (TimeoutException e) { + // Ignore + } catch (AdbCommandRejectedException e) { + // Ignore + } catch (ShellCommandUnresponsiveException e) { + // Ignore + } + } + }.start(); + } + + return false; + } + + /** + * Adds a new filter to the current filter array, and set its colors + * @param newFilter The filter to add + */ + private void addFilterToArray(LogFilter newFilter) { + // set the colors + newFilter.setColors(mColors); + + // add it to the array. + if (mFilters != null && mFilters.length > 0) { + LogFilter[] newFilters = new LogFilter[mFilters.length+1]; + System.arraycopy(mFilters, 0, newFilters, 0, mFilters.length); + newFilters[mFilters.length] = newFilter; + mFilters = newFilters; + } else { + mFilters = new LogFilter[1]; + mFilters[0] = newFilter; + } + } + + private void removeFilterFromArray(LogFilter oldFilter) { + // look for the index + int index = -1; + for (int i = 0 ; i < mFilters.length ; i++) { + if (mFilters[i] == oldFilter) { + index = i; + break; + } + } + + if (index != -1) { + LogFilter[] newFilters = new LogFilter[mFilters.length-1]; + System.arraycopy(mFilters, 0, newFilters, 0, index); + System.arraycopy(mFilters, index + 1, newFilters, index, + newFilters.length-index); + mFilters = newFilters; + } + } + + /** + * Initialize the filter with already existing buffer. + * @param filter + */ + private void initFilter(LogFilter filter) { + // is it empty + if (filter.uiReady() == false) { + return; + } + + if (filter == mDefaultFilter) { + initDefaultFilter(); + return; + } + + filter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + + filter.addMessage(mBuffer[realItemIndex], null /* old message */); + } + } + + filter.flush(); + filter.resetTempFilteringStatus(); + } + + /** + * Refill the default filter. Not to be called directly. + * @see initFilter() + */ + private void initDefaultFilter() { + mDefaultFilter.clear(); + + if (mBufferStart != -1) { + int max = mBufferEnd; + if (mBufferEnd < mBufferStart) { + max += STRING_BUFFER_LENGTH; + } + + for (int i = mBufferStart; i < max; i++) { + int realItemIndex = i % STRING_BUFFER_LENGTH; + LogMessage msg = mBuffer[realItemIndex]; + + // first we check that the other filters don't take this message + boolean filtered = false; + for (LogFilter f : mFilters) { + filtered |= f.accept(msg); + } + + if (filtered == false) { + mDefaultFilter.addMessage(msg, null /* old message */); + } + } + } + + mDefaultFilter.flush(); + mDefaultFilter.resetTempFilteringStatus(); + } + + /** + * Reset the filters, to handle change in device in automatic filter mode + */ + private void resetFilters() { + // if we are in automatic mode, then we need to rmove the current + // filter. + if (mFilterMode == FILTER_AUTO_PID || mFilterMode == FILTER_AUTO_TAG) { + mFilters = null; + + // recreate the filters. + createFilters(); + } + } + + + private LogFilter getCurrentFilter() { + int index = mFolders.getSelectionIndex(); + + // if mFilters is null or index is invalid, we return the default + // filter. It doesn't matter if that one is null as well, since we + // would return null anyway. + if (index == 0 || mFilters == null) { + return mDefaultFilter; + } + + return mFilters[index-1]; + } + + + private void emptyTables() { + for (LogFilter f : mFilters) { + f.getTable().removeAll(); + } + + if (mDefaultFilter != null) { + mDefaultFilter.getTable().removeAll(); + } + } + + protected void updateFilteringWith(String text) { + synchronized (mBuffer) { + // reset the temp filtering for all the filters + for (LogFilter f : mFilters) { + f.resetTempFiltering(); + } + if (mDefaultFilter != null) { + mDefaultFilter.resetTempFiltering(); + } + + // now we need to figure out the new temp filtering + // split each word + String[] segments = text.split(" "); //$NON-NLS-1$ + + ArrayList keywords = new ArrayList(segments.length); + + // loop and look for temp id/tag + int tempPid = -1; + String tempTag = null; + for (int i = 0 ; i < segments.length; i++) { + String s = segments[i]; + if (tempPid == -1 && s.startsWith("pid:")) { //$NON-NLS-1$ + // get the pid + String[] seg = s.split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + if (seg[1].matches("^[0-9]*$")) { //$NON-NLS-1$ + tempPid = Integer.valueOf(seg[1]); + } + } + } else if (tempTag == null && s.startsWith("tag:")) { //$NON-NLS-1$ + String seg[] = segments[i].split(":"); //$NON-NLS-1$ + if (seg.length == 2) { + tempTag = seg[1]; + } + } else { + keywords.add(s); + } + } + + // set the temp filtering in the filters + if (tempPid != -1 || tempTag != null || keywords.size() > 0) { + String[] keywordsArray = keywords.toArray( + new String[keywords.size()]); + + for (LogFilter f : mFilters) { + if (tempPid != -1) { + f.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + f.setTempTagFiltering(tempTag); + } + f.setTempKeywordFiltering(keywordsArray); + } + + if (mDefaultFilter != null) { + if (tempPid != -1) { + mDefaultFilter.setTempPidFiltering(tempPid); + } + if (tempTag != null) { + mDefaultFilter.setTempTagFiltering(tempTag); + } + mDefaultFilter.setTempKeywordFiltering(keywordsArray); + + } + } + + initFilter(mCurrentFilter); + } + } + + /** + * Called when the current filter selection changes. + * @param selectedFilter + */ + private void selectionChanged(LogFilter selectedFilter) { + if (mLogLevelActions != null) { + // get the log level + int level = selectedFilter.getLogLevel(); + for (int i = 0 ; i < mLogLevelActions.length; i++) { + ICommonAction a = mLogLevelActions[i]; + if (i == level - 2) { + a.setChecked(true); + } else { + a.setChecked(false); + } + } + } + + if (mDeleteFilterAction != null) { + mDeleteFilterAction.setEnabled(selectedFilter.supportsDelete()); + } + if (mEditFilterAction != null) { + mEditFilterAction.setEnabled(selectedFilter.supportsEdit()); + } + } + + public String getSelectedErrorLineMessage() { + Table table = mCurrentFilter.getTable(); + int[] selection = table.getSelectionIndices(); + + if (selection.length == 1) { + TableItem item = table.getItem(selection[0]); + LogMessage msg = (LogMessage)item.getData(); + if (msg.data.logLevel == LogLevel.ERROR || msg.data.logLevel == LogLevel.WARN) + return msg.msg; + } + return null; + } + + public void setLogCatViewInterface(LogCatViewInterface i) { + mLogCatViewInterface = i; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java new file mode 100644 index 00000000..65ce49f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.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 mTrackedItems = new ArrayList(); + + private SampleThread mSampleThread; + + private class SampleThread extends Thread { + private volatile boolean mFinish; + + public void finish() { + mFinish = true; + interrupt(); + } + + @Override + public void run() { + while (!mFinish && !mDisplay.isDisposed()) { + performSample(); + + try { + Thread.sleep(mSpeedMillis); + } catch (InterruptedException e) { + // ignored + } + } + } + } + + /** Last snapshot taken by {@link #performSample()}. */ + private NetworkSnapshot mLastSnapshot; + + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + mPanel = new Composite(parent, SWT.NONE); + + final FormLayout formLayout = new FormLayout(); + mPanel.setLayout(formLayout); + + createHeader(); + createChart(); + createTable(); + + return mPanel; + } + + /** + * Create header panel with configuration options. + */ + private void createHeader() { + + mHeader = new Composite(mPanel, SWT.NONE); + final RowLayout layout = new RowLayout(); + layout.center = true; + mHeader.setLayout(layout); + + mSpeedLabel = new Label(mHeader, SWT.NONE); + mSpeedLabel.setText("Speed:"); + mSpeedCombo = new Combo(mHeader, SWT.PUSH); + mSpeedCombo.add("Fast (100ms)"); + mSpeedCombo.add("Medium (250ms)"); + mSpeedCombo.add("Slow (500ms)"); + mSpeedCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateSpeed(); + } + }); + + mSpeedCombo.select(1); + updateSpeed(); + + mRunningButton = new Button(mHeader, SWT.PUSH); + mRunningButton.setText("Start"); + mRunningButton.setEnabled(false); + mRunningButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + final boolean alreadyRunning = mSampleThread != null; + updateRunning(!alreadyRunning); + } + }); + + mResetButton = new Button(mHeader, SWT.PUSH); + mResetButton.setText("Reset"); + mResetButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + clearTrackedItems(); + } + }); + + final FormData data = new FormData(); + data.top = new FormAttachment(0); + data.left = new FormAttachment(0); + data.right = new FormAttachment(100); + mHeader.setLayoutData(data); + } + + /** + * Create chart of recent network activity. + */ + private void createChart() { + + mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false); + + // create backing datasets and series + mRxTotalSeries = new TimeSeries("RX total"); + mTxTotalSeries = new TimeSeries("TX total"); + + mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); + mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); + + mTotalCollection = new TimeSeriesCollection(); + mTotalCollection.addSeries(mRxTotalSeries); + mTotalCollection.addSeries(mTxTotalSeries); + + mRxDetailDataset = new LiveTimeTableXYDataset(); + mTxDetailDataset = new LiveTimeTableXYDataset(); + + mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA); + mRenderer = new StackedXYAreaRenderer2(); + + final XYPlot xyPlot = mChart.getXYPlot(); + + xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD); + + xyPlot.setDataset(0, mTotalCollection); + xyPlot.setDataset(1, mRxDetailDataset); + xyPlot.setDataset(2, mTxDetailDataset); + xyPlot.setRenderer(0, mTotalRenderer); + xyPlot.setRenderer(1, mRenderer); + xyPlot.setRenderer(2, mRenderer); + + // we control domain axis manually when taking samples + mDomainAxis = xyPlot.getDomainAxis(); + mDomainAxis.setAutoRange(false); + + final NumberAxis axis = new NumberAxis(); + axis.setNumberFormatOverride(new BytesFormat(true)); + axis.setAutoRangeMinimumSize(50); + xyPlot.setRangeAxis(axis); + xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); + + // draw thick line to separate RX versus TX traffic + xyPlot.addRangeMarker( + new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2))); + + // label to indicate that positive axis is RX traffic + final ValueMarker rxMarker = new ValueMarker(0); + rxMarker.setStroke(new java.awt.BasicStroke(0)); + rxMarker.setLabel("RX"); + rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f)); + rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); + rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT); + rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT); + xyPlot.addRangeMarker(rxMarker); + + // label to indicate that negative axis is TX traffic + final ValueMarker txMarker = new ValueMarker(0); + txMarker.setStroke(new java.awt.BasicStroke(0)); + txMarker.setLabel("TX"); + txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f)); + txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); + txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT); + txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT); + xyPlot.addRangeMarker(txMarker); + + mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true, + false, true); + + final FormData data = new FormData(); + data.top = new FormAttachment(mHeader); + data.left = new FormAttachment(0); + data.bottom = new FormAttachment(70); + data.right = new FormAttachment(100); + mChartComposite.setLayoutData(data); + } + + /** + * Create table showing summary of network activity. + */ + private void createTable() { + mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION); + + final FormData data = new FormData(); + data.top = new FormAttachment(mChartComposite); + data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER); + data.bottom = new FormAttachment(100); + mTable.setLayoutData(data); + + mTable.setHeaderVisible(true); + mTable.setLinesVisible(true); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null); + TableHelper.createTableColumn( + mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store); + TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_RX_BYTES, store); + TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_RX_PACKETS, store); + TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_TX_BYTES, store); + TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_TX_PACKETS, store); + + mTableViewer = new TableViewer(mTable); + mTableViewer.setContentProvider(new ContentProvider()); + mTableViewer.setLabelProvider(new LabelProvider()); + } + + /** + * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection. + */ + private void updateSpeed() { + switch (mSpeedCombo.getSelectionIndex()) { + case 0: + mSpeedMillis = 100; + break; + case 1: + mSpeedMillis = 250; + break; + case 2: + mSpeedMillis = 500; + break; + } + } + + /** + * Update if {@link SampleThread} should be actively running. Will create + * new thread or finish existing thread to match requested state. + */ + private void updateRunning(boolean shouldRun) { + final boolean alreadyRunning = mSampleThread != null; + if (alreadyRunning && !shouldRun) { + mSampleThread.finish(); + mSampleThread = null; + + mRunningButton.setText("Start"); + mHeader.pack(); + } else if (!alreadyRunning && shouldRun) { + mSampleThread = new SampleThread(); + mSampleThread.start(); + + mRunningButton.setText("Stop"); + mHeader.pack(); + } + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + + private static java.awt.Color nextSeriesColor(int index) { + return SERIES_COLORS[index % SERIES_COLORS.length]; + } + + /** + * Find a {@link TrackedItem} that matches the requested UID and tag, or + * create one if none exists. + */ + public TrackedItem findOrCreateTrackedItem(int uid, int tag) { + // try searching for existing item + for (TrackedItem item : mTrackedItems) { + if (item.uid == uid && item.tag == tag) { + return item; + } + } + + // nothing found; create new item + final TrackedItem item = new TrackedItem(uid, tag); + if (item.isTotal()) { + item.color = TOTAL_COLOR; + item.label = "Total"; + } else { + final int size = mTrackedItems.size(); + item.color = nextSeriesColor(size); + Formatter formatter = new Formatter(); + item.label = "0x" + formatter.format("%08x", tag); + formatter.close(); + } + + // create color chip to display as legend in table + item.colorImage = new Image(mDisplay, 20, 20); + final GC gc = new GC(item.colorImage); + gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color + .getRed(), item.color.getGreen(), item.color.getBlue())); + gc.fillRectangle(item.colorImage.getBounds()); + gc.dispose(); + + mTrackedItems.add(item); + return item; + } + + /** + * Clear all {@link TrackedItem} and chart history. + */ + public void clearTrackedItems() { + mRxTotalSeries.clear(); + mTxTotalSeries.clear(); + + mRxDetailDataset.clear(); + mTxDetailDataset.clear(); + + mTrackedItems.clear(); + mTableViewer.setInput(mTrackedItems); + } + + /** + * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}. + */ + private void updateSeriesPaint() { + for (TrackedItem item : mTrackedItems) { + final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label); + if (seriesIndex >= 0) { + mRenderer.setSeriesPaint(seriesIndex, item.color); + mRenderer.setSeriesFillPaint(seriesIndex, item.color); + } + } + + // series data is always the same color + final int count = mTotalCollection.getSeriesCount(); + for (int i = 0; i < count; i++) { + mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR); + mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR); + } + } + + /** + * Traffic flow being actively tracked, uniquely defined by UID and tag. Can + * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for + * charting, and into summary statistics for {@link Table} display. + */ + private class TrackedItem { + public final int uid; + public final int tag; + + public java.awt.Color color; + public Image colorImage; + + public String label; + public long rxBytes; + public long rxPackets; + public long txBytes; + public long txPackets; + + public TrackedItem(int uid, int tag) { + this.uid = uid; + this.tag = tag; + } + + public boolean isTotal() { + return tag == 0x0; + } + + /** + * Record the given {@link NetworkSnapshot} delta, updating + * {@link TimeSeries} and summary statistics. + * + * @param time Timestamp when delta was observed. + * @param deltaMillis Time duration covered by delta, in milliseconds. + */ + public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) { + final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis; + final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis; + + // record values under correct series + if (isTotal()) { + mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond); + mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond); + } else { + mRxDetailDataset.addValue(rxBytesPerSecond, time, label); + mTxDetailDataset.addValue(-txBytesPerSecond, time, label); + } + + rxBytes += delta.rxBytes; + rxPackets += delta.rxPackets; + txBytes += delta.txBytes; + txPackets += delta.txPackets; + } + } + + @Override + public void deviceSelected() { + // treat as client selection to update enabled states + clientSelected(); + } + + @Override + public void clientSelected() { + mActiveUid = -1; + + final Client client = getCurrentClient(); + if (client != null) { + final int pid = client.getClientData().getPid(); + try { + // map PID to UID from device + final UidParser uidParser = new UidParser(); + getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser); + mActiveUid = uidParser.uid; + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + clearTrackedItems(); + updateRunning(false); + + final boolean validUid = mActiveUid != -1; + mRunningButton.setEnabled(validUid); + } + + @Override + public void clientChanged(Client client, int changeMask) { + // ignored + } + + /** + * Take a snapshot from {@link #getCurrentDevice()}, recording any delta + * network traffic to {@link TrackedItem}. + */ + public void performSample() { + final IDevice device = getCurrentDevice(); + if (device == null) return; + + try { + final NetworkSnapshotParser parser = new NetworkSnapshotParser(); + device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser); + + if (parser.isError()) { + mDisplay.asyncExec(new Runnable() { + @Override + public void run() { + updateRunning(false); + + final String title = "Problem reading stats"; + final String message = "Problem reading xt_qtaguid network " + + "statistics from selected device."; + Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null); + ErrorDialog.openError(mPanel.getShell(), title, title, status); + } + }); + + return; + } + + final NetworkSnapshot snapshot = parser.getParsedSnapshot(); + + // use first snapshot as baseline + if (mLastSnapshot == null) { + mLastSnapshot = snapshot; + return; + } + + final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot); + mLastSnapshot = snapshot; + + // perform delta updates over on UI thread + if (!mDisplay.isDisposed()) { + mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp)); + } + + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Task that updates UI with given {@link NetworkSnapshot} delta. + */ + private class UpdateDeltaRunnable implements Runnable { + private final NetworkSnapshot mDelta; + private final long mEndTime; + + public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) { + mDelta = delta; + mEndTime = endTime; + } + + @Override + public void run() { + if (mDisplay.isDisposed()) return; + + final Millisecond time = new Millisecond(new Date(mEndTime)); + for (NetworkSnapshot.Entry entry : mDelta) { + if (mActiveUid != entry.uid) continue; + + final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag); + item.recordDelta(time, mDelta.timestamp, entry); + } + + // remove any historical detail data + final long beforeMillis = mEndTime - HISTORY_MILLIS; + mRxDetailDataset.removeBefore(beforeMillis); + mTxDetailDataset.removeBefore(beforeMillis); + + // trigger refresh from bulk changes above + mRxDetailDataset.fireDatasetChanged(); + mTxDetailDataset.fireDatasetChanged(); + + // update axis to show latest 30 second time period + mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime); + + updateSeriesPaint(); + + // kick table viewer to update + mTableViewer.setInput(mTrackedItems); + } + } + + /** + * Parser that extracts UID from remote {@code /proc/pid/status} file. + */ + private static class UidParser extends MultiLineReceiver { + public int uid = -1; + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.startsWith("Uid:")) { + // we care about the "real" UID + final String[] cols = line.split("\t"); + uid = Integer.parseInt(cols[1]); + } + } + } + } + + /** + * Parser that populates {@link NetworkSnapshot} based on contents of remote + * {@link NetworkPanel#PROC_XT_QTAGUID} file. + */ + private static class NetworkSnapshotParser extends MultiLineReceiver { + private NetworkSnapshot mSnapshot; + + public NetworkSnapshotParser() { + mSnapshot = new NetworkSnapshot(System.currentTimeMillis()); + } + + public boolean isError() { + return mSnapshot == null; + } + + public NetworkSnapshot getParsedSnapshot() { + return mSnapshot; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.endsWith("No such file or directory")) { + mSnapshot = null; + return; + } + + // ignore header line + if (line.startsWith("idx")) { + continue; + } + + final String[] cols = line.split(" "); + if (cols.length < 9) continue; + + // iface and set are currently ignored, which groups those + // entries together. + final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry(); + + entry.iface = null; //cols[1]; + entry.uid = Integer.parseInt(cols[3]); + entry.set = -1; //Integer.parseInt(cols[4]); + entry.tag = kernelToTag(cols[2]); + entry.rxBytes = Long.parseLong(cols[5]); + entry.rxPackets = Long.parseLong(cols[6]); + entry.txBytes = Long.parseLong(cols[7]); + entry.txPackets = Long.parseLong(cols[8]); + + mSnapshot.combine(entry); + } + } + + /** + * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming + * format like {@code 0x7fffffff00000000}. + * Matches code in android.server.NetworkManagementSocketTagger + */ + public static int kernelToTag(String string) { + int length = string.length(); + if (length > 10) { + return Long.decode(string.substring(0, length - 8)).intValue(); + } else { + return 0; + } + } + } + + /** + * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time. + */ + private static class NetworkSnapshot implements Iterable { + private ArrayList mStats = new ArrayList(); + + public final long timestamp; + + /** Single parsed statistics row. */ + public static class Entry { + public String iface; + public int uid; + public int set; + public int tag; + public long rxBytes; + public long rxPackets; + public long txBytes; + public long txPackets; + + public boolean isEmpty() { + return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0; + } + } + + public NetworkSnapshot(long timestamp) { + this.timestamp = timestamp; + } + + public void clear() { + mStats.clear(); + } + + /** + * Combine the given {@link Entry} with any existing {@link Entry}, or + * insert if none exists. + */ + public void combine(Entry entry) { + final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag); + if (existing != null) { + existing.rxBytes += entry.rxBytes; + existing.rxPackets += entry.rxPackets; + existing.txBytes += entry.txBytes; + existing.txPackets += entry.txPackets; + } else { + mStats.add(entry); + } + } + + @Override + public Iterator iterator() { + return mStats.iterator(); + } + + public Entry findEntry(String iface, int uid, int set, int tag) { + for (Entry entry : mStats) { + if (entry.uid == uid && entry.set == set && entry.tag == tag + && equal(entry.iface, iface)) { + return entry; + } + } + return null; + } + + /** + * Subtract the two given {@link NetworkSnapshot} objects, returning the + * delta between them. + */ + public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) { + final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp); + + // for each row on left, subtract value from right side + for (Entry leftEntry : left) { + final Entry rightEntry = right.findEntry( + leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag); + if (rightEntry == null) continue; + + final Entry resultEntry = new Entry(); + resultEntry.iface = leftEntry.iface; + resultEntry.uid = leftEntry.uid; + resultEntry.set = leftEntry.set; + resultEntry.tag = leftEntry.tag; + resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes; + resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets; + resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes; + resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets; + + result.combine(resultEntry); + } + + return result; + } + } + + /** + * Provider of {@link #mTrackedItems}. + */ + private class ContentProvider implements IStructuredContentProvider { + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public Object[] getElements(Object inputElement) { + return mTrackedItems.toArray(); + } + } + + /** + * Provider of labels for {@Link TrackedItem} values. + */ + private static class LabelProvider implements ITableLabelProvider { + private final DecimalFormat mFormat = new DecimalFormat("#,###"); + + @Override + public Image getColumnImage(Object element, int columnIndex) { + if (element instanceof TrackedItem) { + final TrackedItem item = (TrackedItem) element; + switch (columnIndex) { + case 0: + return item.colorImage; + } + } + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof TrackedItem) { + final TrackedItem item = (TrackedItem) element; + switch (columnIndex) { + case 0: + return null; + case 1: + return item.label; + case 2: + return mFormat.format(item.rxBytes); + case 3: + return mFormat.format(item.rxPackets); + case 4: + return mFormat.format(item.txBytes); + case 5: + return mFormat.format(item.txPackets); + } + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Format that displays simplified byte units for when given values are + * large enough. + */ + private static class BytesFormat extends NumberFormat { + private final String[] mUnits; + private final DecimalFormat mFormat = new DecimalFormat("#.#"); + + public BytesFormat(boolean perSecond) { + if (perSecond) { + mUnits = new String[] { "B/s", "KB/s", "MB/s" }; + } else { + mUnits = new String[] { "B", "KB", "MB" }; + } + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + double value = Math.abs(number); + + int i = 0; + while (value > 1024 && i < mUnits.length - 1) { + value /= 1024; + i++; + } + + toAppendTo.append(mFormat.format(value)); + toAppendTo.append(mUnits[i]); + + return toAppendTo; + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + return format((long) number, toAppendTo, pos); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + return null; + } + } + + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Build stub string of requested length, usually for measurement. + */ + private static String buildSampleText(int length) { + final StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append("X"); + } + return builder.toString(); + } + + /** + * Dataset that contains live measurements. Exposes + * {@link #removeBefore(long)} to efficiently remove old data, and enables + * batched {@link #fireDatasetChanged()} events. + */ + public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements + TableXYDataset { + private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true); + + /** + * Caller is responsible for triggering {@link #fireDatasetChanged()}. + */ + public void addValue(Number value, TimePeriod rowKey, String columnKey) { + mValues.addValue(value, rowKey, columnKey); + } + + /** + * Caller is responsible for triggering {@link #fireDatasetChanged()}. + */ + public void removeBefore(long beforeMillis) { + while(mValues.getRowCount() > 0) { + final TimePeriod period = (TimePeriod) mValues.getRowKey(0); + if (period.getEnd().getTime() < beforeMillis) { + mValues.removeRow(0); + } else { + break; + } + } + } + + public int getColumnIndex(String key) { + return mValues.getColumnIndex(key); + } + + public void clear() { + mValues.clear(); + fireDatasetChanged(); + } + + @Override + public void fireDatasetChanged() { + super.fireDatasetChanged(); + } + + @Override + public int getItemCount() { + return mValues.getRowCount(); + } + + @Override + public int getItemCount(int series) { + return mValues.getRowCount(); + } + + @Override + public int getSeriesCount() { + return mValues.getColumnCount(); + } + + @Override + public Comparable getSeriesKey(int series) { + return mValues.getColumnKey(series); + } + + @Override + public double getXValue(int series, int item) { + final TimePeriod period = (TimePeriod) mValues.getRowKey(item); + return period.getStart().getTime(); + } + + @Override + public double getStartXValue(int series, int item) { + return getXValue(series, item); + } + + @Override + public double getEndXValue(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getStartX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getEndX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getY(int series, int item) { + return mValues.getValue(item, series); + } + + @Override + public Number getStartY(int series, int item) { + return getY(series, item); + } + + @Override + public Number getEndY(int series, int item) { + return getY(series, item); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java new file mode 100644 index 00000000..c4161dcc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.screenrecord; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.ScreenRecorderOptions; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Shell; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class ScreenRecorderAction { + private static final String TITLE = "Screen Recorder"; + private static final String REMOTE_PATH = "/sdcard/ddmsrec.mp4"; + + private final Shell mParentShell; + private final IDevice mDevice; + + public ScreenRecorderAction(Shell parent, IDevice device) { + mParentShell = parent; + mDevice = device; + } + + public void performAction() { + ScreenRecorderOptionsDialog optionsDialog = new ScreenRecorderOptionsDialog(mParentShell); + if (optionsDialog.open() == Window.CANCEL) { + return; + } + + final ScreenRecorderOptions options = new ScreenRecorderOptions.Builder() + .setBitRate(optionsDialog.getBitRate()) + .setSize(optionsDialog.getWidth(), optionsDialog.getHeight()) + .build(); + + final CountDownLatch latch = new CountDownLatch(1); + final CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch); + + new Thread(new Runnable() { + @Override + public void run() { + try { + mDevice.startScreenRecorder(REMOTE_PATH, options, receiver); + } catch (Exception e) { + showError("Unexpected error while launching screenrecorder", e); + latch.countDown(); + } + } + }, "Screen Recorder").start(); + + try { + new ProgressMonitorDialog(mParentShell).run(true, true, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException { + int timeInSecond = 0; + monitor.beginTask("Recording...", IProgressMonitor.UNKNOWN); + + while (true) { + // Wait for a second to see if the command has completed + if (latch.await(1, TimeUnit.SECONDS)) { + break; + } + + // update recording time in second + monitor.subTask(String.format("Recording...%d seconds elapsed", timeInSecond++)); + + // If not, check if user has cancelled + if (monitor.isCanceled()) { + receiver.cancel(); + + monitor.subTask("Stopping..."); + + // wait for an additional second to make sure that the command + // completed and screenrecorder finishes writing the output + latch.await(1, TimeUnit.SECONDS); + break; + } + } + } + }); + } catch (InvocationTargetException e) { + showError("Unexpected error while recording: ", e.getTargetException()); + return; + } catch (InterruptedException ignored) { + } + + try { + mDevice.pullFile(REMOTE_PATH, optionsDialog.getDestination().getAbsolutePath()); + } catch (Exception e) { + showError("Unexpected error while copying video recording from device", e); + } + + MessageDialog.openInformation(mParentShell, TITLE, "Screen recording saved at " + + optionsDialog.getDestination().getAbsolutePath()); + } + + private void showError(@NonNull final String message, @Nullable final Throwable e) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + String msg = message; + if (e != null) { + msg += e.getLocalizedMessage() != null ? ": " + e.getLocalizedMessage() : ""; + } + MessageDialog.openError(mParentShell, TITLE, msg); + } + }); + + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java new file mode 100644 index 00000000..ccca3df1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.screenrecord; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.util.Calendar; + +public class ScreenRecorderOptionsDialog extends TitleAreaDialog { + private static final int DEFAULT_BITRATE_MBPS = 4; + + private static String sLastSavedFolder = System.getProperty("user.home"); + private static String sLastFileName = suggestFileName(); + + private static int sBitRateMbps = DEFAULT_BITRATE_MBPS; + private static int sWidth = 0; + private static int sHeight = 0; + + private Text mBitRateText; + private Text mWidthText; + private Text mHeightText; + private Text mDestinationText; + + public ScreenRecorderOptionsDialog(Shell parentShell) { + super(parentShell); + setShellStyle(getShellStyle() | SWT.RESIZE); + } + + @Override + protected Control createDialogArea(Composite shell) { + setTitle("Screen Recorder Options"); + setMessage("Provide screen recorder options. Leave empty to use defaults."); + + Composite parent = (Composite) super.createDialogArea(shell); + Composite c = new Composite(parent, SWT.BORDER); + c.setLayout(new GridLayout(3, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createLabel(c, "Bit Rate (in Mbps)"); + mBitRateText = new Text(c, SWT.BORDER); + mBitRateText.setText(Integer.toString(sBitRateMbps)); + mBitRateText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + createLabel(c, ""); // empty label for 3rd column + + createLabel(c, "Video width (in px, defaults to screen width)"); + mWidthText = new Text(c, SWT.BORDER); + mWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (sWidth > 0) { + mWidthText.setText(Integer.toString(sWidth)); + } + createLabel(c, ""); // empty label for 3rd column + + createLabel(c, "Video height (in px, defaults to screen height)"); + mHeightText = new Text(c, SWT.BORDER); + mHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (sHeight > 0) { + mHeightText.setText(Integer.toString(sHeight)); + } + createLabel(c, ""); // empty label for 3rd column + + ModifyListener m = new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + validateAndUpdateState(); + } + }; + mBitRateText.addModifyListener(m); + mWidthText.addModifyListener(m); + mHeightText.addModifyListener(m); + + createLabel(c, "Save Video as: "); + mDestinationText = new Text(c, SWT.BORDER); + mDestinationText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDestinationText.setText(getFilePath()); + + Button browseButton = new Button(c, SWT.PUSH); + browseButton.setText("Browse"); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent selectionEvent) { + FileDialog dlg = new FileDialog(getShell(), SWT.SAVE); + + dlg.setText("Save Video..."); + dlg.setFileName(sLastFileName != null ? sLastFileName : suggestFileName()); + if (sLastSavedFolder != null) { + dlg.setFilterPath(sLastSavedFolder); + } + dlg.setFilterNames(new String[] { "MP4 files (*.mp4)" }); + dlg.setFilterExtensions(new String[] { "*.mp4" }); + + String filePath = dlg.open(); + if (filePath != null) { + if (!filePath.endsWith(".mp4")) { + filePath += ".mp4"; + } + + mDestinationText.setText(filePath); + validateAndUpdateState(); + } + } + }); + + return c; + } + + private static String getFilePath() { + return sLastSavedFolder + File.separatorChar + sLastFileName; + } + + private static String suggestFileName() { + Calendar now = Calendar.getInstance(); + return String.format("device-%tF-%tH%tM%tS.mp4", now, now, now, now); + } + + private void createLabel(Composite c, String text) { + Label l = new Label(c, SWT.NONE); + l.setText(text); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + l.setLayoutData(gd); + } + + private void validateAndUpdateState() { + int intValue; + + if ((intValue = validateInteger(mBitRateText.getText().trim(), + "Bit Rate has to be an integer")) < 0) { + return; + } + sBitRateMbps = intValue > 0 ? intValue : DEFAULT_BITRATE_MBPS; + + if ((intValue = validateInteger(mWidthText.getText().trim(), + "Recorded video resolution width has to be a valid integer.")) < 0) { + return; + } + if (intValue % 16 != 0) { + setErrorMessage("Width must be a multiple of 16"); + setOkButtonEnabled(false); + return; + } + sWidth = intValue; + + if ((intValue = validateInteger(mHeightText.getText().trim(), + "Recorded video resolution height has to be a valid integer.")) < 0) { + return; + } + if (intValue % 16 != 0) { + setErrorMessage("Height must be a multiple of 16"); + setOkButtonEnabled(false); + return; + } + sHeight = intValue; + + String filePath = mDestinationText.getText(); + File f = new File(filePath); + if (!f.getParentFile().isDirectory()) { + setErrorMessage("The path '" + f.getParentFile().getAbsolutePath() + + "' is not a valid directory."); + setOkButtonEnabled(false); + return; + } + sLastFileName = f.getName(); + sLastSavedFolder = f.getParentFile().getAbsolutePath(); + + setErrorMessage(null); + setOkButtonEnabled(true); + } + + private int validateInteger(String s, String errorMessage) { + if (!s.isEmpty()) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + setErrorMessage(errorMessage); + setOkButtonEnabled(false); + return -1; + } + } + + return 0; + } + + private void setOkButtonEnabled(boolean en) { + getButton(IDialogConstants.OK_ID).setEnabled(en); + } + + public int getBitRate() { + return sBitRateMbps; + } + + public int getWidth() { + return sWidth; + } + + public int getHeight() { + return sHeight; + } + + public File getDestination() { + return new File(sLastSavedFolder, sLastFileName); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java new file mode 100644 index 00000000..9a65ba18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.vmtrace; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** Dialog that allows users to select between method tracing or sampler based profiling. */ +public class VmTraceOptionsDialog extends Dialog { + private static final int DEFAULT_SAMPLING_INTERVAL_US = 1000; + + // Static variables that maintain state across invocations of the dialog + private static boolean sTracingEnabled = false; + private static int sSamplingIntervalUs = DEFAULT_SAMPLING_INTERVAL_US; + + public VmTraceOptionsDialog(Shell parentShell) { + super(parentShell); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Profiling Options"); + } + + @Override + protected Control createDialogArea(Composite shell) { + int horizontalIndent = 30; + + Composite parent = (Composite) super.createDialogArea(shell); + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(2, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + final Button useSamplingButton = new Button(c, SWT.RADIO); + useSamplingButton.setText("Sample based profiling"); + useSamplingButton.setSelection(!sTracingEnabled); + GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + useSamplingButton.setLayoutData(gd); + + Label l = new Label(c, SWT.NONE); + l.setText("Sample based profiling works by interrupting the VM at a given frequency and \n" + + "collecting the call stacks at that time. The overhead is proportional to the \n" + + "sampling frequency."); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + l = new Label(c, SWT.NONE); + l.setText("Sampling frequency (microseconds): "); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_END, + false, true); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + final Text samplingIntervalTextField = new Text(c, SWT.BORDER); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true); + gd.widthHint = 100; + samplingIntervalTextField.setLayoutData(gd); + samplingIntervalTextField.setEnabled(!sTracingEnabled); + samplingIntervalTextField.setText(Integer.toString(sSamplingIntervalUs)); + samplingIntervalTextField.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + int v = getIntegerValue(samplingIntervalTextField.getText()); + getButton(IDialogConstants.OK_ID).setEnabled(v > 0); + sSamplingIntervalUs = v > 0 ? v : DEFAULT_SAMPLING_INTERVAL_US; + } + + private int getIntegerValue(String text) { + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return -1; + } + } + }); + + final Button useTracingButton = new Button(c, SWT.RADIO); + useTracingButton.setText("Trace based profiling"); + useTracingButton.setSelection(sTracingEnabled); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, + GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); + useTracingButton.setLayoutData(gd); + + l = new Label(c, SWT.NONE); + l.setText("Trace based profiling works by tracing the entry and exit of every method.\n" + + "This captures the execution of all methods, no matter how small, and hence\n" + + "has a high overhead."); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + SelectionAdapter selectionAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + sTracingEnabled = useTracingButton.getSelection(); + samplingIntervalTextField.setEnabled(!sTracingEnabled); + } + }; + useTracingButton.addSelectionListener(selectionAdapter); + useSamplingButton.addSelectionListener(selectionAdapter); + + return c; + } + + public boolean shouldUseTracing() { + return sTracingEnabled; + } + + public int getSamplingIntervalMicros() { + return sSamplingIntervalUs; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java new file mode 100644 index 00000000..2977390e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java @@ -0,0 +1,196 @@ +/* + * 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 java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class BugReportParserTest { + @Test + 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 data = BugReportParser.readCpuDataset(br); + + assertEquals(4, data.size()); + assertEquals("system_server (user)", data.get(0).name); + assertEquals("Idle", data.get(3).name); + } + + @Test + public void testParseJbCpuDataSet() throws IOException { + String cpuInfo = + "Load: 1.0 / 1.02 / 0.97\n" + + "CPU usage from 96307ms to 36303ms ago:\n" + + " 0.4% 675/system_server: 0.3% user + 0.1% kernel / faults: 198 minor\n" + + " 0.1% 173/mpdecision: 0% user + 0.1% kernel\n" + + " 0% 2856/kworker/0:2: 0% user + 0% kernel\n" + + " 0% 3128/kworker/0:0: 0% user + 0% kernel\n" + + "0.3% TOTAL: 0.1% user + 0% kernel + 0% iowait\n"; + BufferedReader br = new BufferedReader(new StringReader(cpuInfo)); + List data = BugReportParser.readCpuDataset(br); + + assertEquals(4, data.size()); + assertEquals("675/system_server (user)", data.get(0).name); + assertEquals("Idle", data.get(3).name); + } + + @Test + public void testParseProcRankEclair() throws IOException { + String memInfo = + " 51 39408K 37908K 18731K 14936K system_server\n" + + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + + " 27 248K 248K 83K 76K /system/bin/debuggerd\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readProcRankDataset(br, + " PID Vss Rss Pss Uss cmdline\n"); + + assertEquals(3, data.size()); + assertEquals("debuggerd", data.get(2).name); + if (data.get(0).value - 18731 > 0.0002) { + fail("Unexpected PSS Value " + data.get(0).value); + } + } + + @Test + public void testParseProcRankJb() throws IOException { + String memInfo = + " 675 101120K 100928K 63452K 52624K system_server\n" + + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + + " ------ ------ ------\n" + + " 480598K 394172K TOTAL\n" + + "\n" + + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readProcRankDataset(br, + " PID Vss Rss Pss Uss cmdline\n"); + + assertEquals(3, data.size()); + } + + @Test + public void testParseMeminfoEclair() throws IOException { + String memInfo = + "------ MEMORY INFO ------\n" + + "MemTotal: 516528 kB\n" + + "MemFree: 401036 kB\n" + + "Buffers: 0 kB\n" + + " PID Vss Rss Pss Uss cmdline\n" + + " 51 39408K 37908K 18731K 14936K system_server\n" + + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + + " 297 23348K 23348K 5245K 2276K com.android.gallery\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readMeminfoDataset(br); + assertEquals(5, data.size()); + + assertEquals("Free", data.get(0).name); + } + + @Test + public void testParseMeminfoJb() throws IOException { + + String memInfo = // note: This dataset does not have all entries, so the totals will be off + "------ MEMORY INFO ------\n" + + "MemTotal: 1916984 kB\n" + + "MemFree: 888048 kB\n" + + "Buffers: 72036 kB\n" + + " PID Vss Rss Pss Uss cmdline\n" + + " 675 101120K 100928K 63452K 52624K system_server\n" + + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + + " ------ ------ ------\n" + + " 480598K 394172K TOTAL\n" + + "\n" + + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; + + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readMeminfoDataset(br); + + assertEquals(6, data.size()); + } + + @Test + public void testParseGfxInfo() throws IOException { + String gfxinfo = + "Applications Graphics Acceleration Info:\n" + + "Uptime: 78455570 Realtime: 78455565\n" + + "\n" + + "** Graphics info for pid 20517 [com.android.launcher] **\n" + + "\n" + + "Recent DisplayList operations\n" + + " DrawDisplayList\n" + + " \n" + + " RestoreToCount\n" + + "\n" + + "Caches:\n" + + "Current memory usage / total memory usage (bytes):\n" + + " TextureCache 4663920 / 25165824\n" + + " \n" + + " FontRenderer 0 262144 / 262144\n" + + "Other:\n" + + " FboCache 2 / 16\n" + + " PatchCache 9 / 512\n" + + "Total memory usage:\n" + + " 13274756 bytes, 12.66 MB\n" + + "\n" + + "Profile data in ms:\n" + + "\n" + + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + + " Draw Process Execute\n" + + " 0.85 1.10 0.61\n" + + " 54.45 0.85 0.52\n" + + " 1.04 2.17 0.73\n" + + " 0.15 0.46 1.01\n" + + "\n" + + "View hierarchy:\n" + + "\n" + + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + + " 276 views, 27.16 kB of display lists, 228 frames rendered\n" + + "\n" + + "\n" + + "Total ViewRootImpl: 1\n" + + "Total Views: 276\n" + + "Total DisplayList: 27.16 kB\n"; + + BufferedReader br = new BufferedReader(new StringReader(gfxinfo)); + List gfxProfile = BugReportParser.parseGfxInfo(br); + + assertEquals(4, gfxProfile.size()); + assertEquals(0.85, gfxProfile.get(0).draw, 0.01); + assertEquals(1.01, gfxProfile.get(3).execute, 0.01); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java new file mode 100644 index 00000000..ea193eba --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java @@ -0,0 +1,76 @@ +/* + * 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.NullProgressMonitor; + +import java.io.StringReader; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class NativeHeapDataImporterTest { + 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; + + @Test + public void testImportValidAllocation() { + mImporter = createImporter(BASIC_TEXT); + try { + mImporter.run(new NullProgressMonitor()); + } catch (InvocationTargetException e) { + fail("Unexpected exception while parsing text: " + e.getTargetException().getMessage()); + } catch (InterruptedException e) { + fail("Tests are not interrupted!"); + } + + NativeHeapSnapshot snapshot = mImporter.getImportedSnapshot(); + assertNotNull(snapshot); + + // check whether all details have been parsed correctly + assertEquals(1, snapshot.getAllocations().size()); + + NativeAllocationInfo info = snapshot.getAllocations().get(0); + + assertEquals(1, info.getAllocationCount()); + assertEquals(524292, info.getSize()); + assertEquals(true, info.isStackCallResolved()); + + List stack = info.getResolvedStackCall(); + assertEquals(4, stack.size()); + } + + private NativeHeapDataImporter createImporter(String contentsToParse) { + StringReader r = new StringReader(contentsToParse); + return new NativeHeapDataImporter(r); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java new file mode 100644 index 00000000..5cc756c1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java @@ -0,0 +1,77 @@ +/* + * 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 org.junit.Test; +import static org.junit.Assert.*; + +public class LogCatFilterSettingsSerializerTest { + /* test that decode(encode(f)) = f */ + public void testSerializer() { + LogCatFilter fs = new LogCatFilter( + "TestFilter", //$NON-NLS-1$ + "Tag'.*Regex", //$NON-NLS-1$ + "regexForTextField..''", //$NON-NLS-1$ + "123", //$NON-NLS-1$ + "TestAppName.*", //$NON-NLS-1$ + LogLevel.ERROR); + + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String s = serializer.encodeToPreferenceString(Arrays.asList(fs), + new HashMap()); + List decodedFiltersList = serializer.decodeFromPreferenceString(s); + + assertEquals(1, decodedFiltersList.size()); + + LogCatFilter dfs = decodedFiltersList.get(0); + assertEquals(fs.getName(), dfs.getName()); + assertEquals(fs.getTag(), dfs.getTag()); + assertEquals(fs.getText(), dfs.getText()); + assertEquals(fs.getPid(), dfs.getPid()); + assertEquals(fs.getAppName(), dfs.getAppName()); + assertEquals(fs.getLogLevel(), dfs.getLogLevel()); + } + + /* test that transient filters are not persisted */ + @Test + public void testTransientFilters() { + LogCatFilter fs = new LogCatFilter( + "TestFilter", //$NON-NLS-1$ + "Tag'.*Regex", //$NON-NLS-1$ + "regexForTextField..''", //$NON-NLS-1$ + "123", //$NON-NLS-1$ + "TestAppName.*", //$NON-NLS-1$ + LogLevel.ERROR); + LogCatFilterData fd = new LogCatFilterData(fs); + fd.setTransient(); + HashMap fdMap = + new HashMap(); + fdMap.put(fs, fd); + + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String s = serializer.encodeToPreferenceString(Arrays.asList(fs), fdMap); + List decodedFiltersList = serializer.decodeFromPreferenceString(s); + + assertEquals(0, decodedFiltersList.size()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java new file mode 100644 index 00000000..b9a588fd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java @@ -0,0 +1,61 @@ +/* + * 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 org.junit.Test; +import static org.junit.Assert.*; + +import org.junit.Before; + +public class LogCatStackTraceParserTest { + 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); + + @Before + public void setUp() throws Exception { + mTranslator = new LogCatStackTraceParser(); + } + + @Test + 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$ + } + + @Test + public void testGetMethodName() { + assertEquals(SAMPLE_METHOD, mTranslator.getMethodName(SAMPLE_TRACE)); + } + + @Test + public void testGetFileName() { + assertEquals(SAMPLE_FNAME, mTranslator.getFileName(SAMPLE_TRACE)); + } + + @Test + public void testGetLineNumber() { + assertEquals(SAMPLE_LINENUM, mTranslator.getLineNumber(SAMPLE_TRACE)); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java new file mode 100644 index 00000000..75e1df84 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ddmuilib.logcat; + +import com.android.ddmuilib.AbstractBufferFindTarget; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class RollingBufferFindTest { + public class FindTarget extends AbstractBufferFindTarget { + private int mSelectedItem = -1; + private int mItemReadCount = 0; + private List mItems = Arrays.asList( + "abc", + "def", + "abc", + null, + "xyz" + ); + + @Override + public int getItemCount() { + return mItems.size(); + } + + @Override + public String getItem(int index) { + mItemReadCount++; + return mItems.get(index); + } + + @Override + public void selectAndReveal(int index) { + mSelectedItem = index; + } + + @Override + public int getStartingIndex() { + return mItems.size() - 1; + } + } + FindTarget mFindTarget = new FindTarget(); + + @Test + 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); + } + + @Test + 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); + } + + @Test + 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); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project new file mode 100644 index 00000000..2be391f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.hierarchyviewer2lib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..6e1641b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF @@ -0,0 +1,22 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer2lib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.andmore.swtmenubar, + org.eclipse.andmore.ddmuilib, + org.eclipse.core.runtime;bundle-version="3.12.0" +Export-Package: com.android.hierarchyviewerlib, + com.android.hierarchyviewerlib.actions, + com.android.hierarchyviewerlib.device, + com.android.hierarchyviewerlib.models, + com.android.hierarchyviewerlib.ui +Bundle-ClassPath: . +Import-Package: org.eclipse.ui.plugin diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/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/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle new file mode 100644 index 00000000..55308716 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle @@ -0,0 +1,16 @@ +group = 'com.android.tools' +archivesBaseName = 'hierarchyviewer2lib' + +dependencies { + compile project(':base:ddmlib') + compile project(':swt:ddmuilib') + + compile 'com.google.guava:guava:15.0' + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml new file mode 100644 index 00000000..206f6712 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties new file mode 100644 index 00000000..16ab8145 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.hierarchyviewer2lib +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Hierarchy Viewer 2 library +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml new file mode 100644 index 00000000..b7c2527b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.hierarchyviewer2lib + eclipse-plugin + hierarchyviewer2lib + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java new file mode 100644 index 00000000..1f8f2e6e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java @@ -0,0 +1,804 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.DeviceBridge; +import com.android.hierarchyviewerlib.device.HvDeviceFactory; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.device.WindowUpdater; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.CaptureDisplay; +import com.android.hierarchyviewerlib.ui.DumpThemeDisplay; +import com.android.hierarchyviewerlib.ui.EvaluateContrastDisplay; +import com.android.hierarchyviewerlib.ui.TreeView; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +/** + * This is the class where most of the logic resides. + */ +public abstract class HierarchyViewerDirector implements IDeviceChangeListener, + IWindowChangeListener { + private static final boolean sIsUsingDdmProtocol; + static { + String sHvProtoEnvVar = System.getenv("ANDROID_HVPROTO"); //$NON-NLS-1$ + sIsUsingDdmProtocol = "ddm".equalsIgnoreCase(sHvProtoEnvVar); + } + + protected static HierarchyViewerDirector sDirector; + + private ImageFactory mImageFactory; + + public static final String TAG = "hierarchyviewer"; + + private int mPixelPerfectRefreshesInProgress = 0; + + private Timer mPixelPerfectRefreshTimer = new Timer(); + + private boolean mAutoRefresh = false; + + public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5; + + private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL; + + private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask; + + private String mFilterText = ""; //$NON-NLS-1$ + + private static final Object mDevicesLock = new Object(); + private Map mDevices = new HashMap(10); + + public HierarchyViewerDirector(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + public static boolean isUsingDdmProtocol() { + return sIsUsingDdmProtocol; + } + + public void terminate() { + WindowUpdater.terminate(); + mPixelPerfectRefreshTimer.cancel(); + mImageFactory.dispose(); + } + + public abstract String getAdbLocation(); + + public static HierarchyViewerDirector getDirector() { + return sDirector; + } + + /** + * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. + * @param bridge the bridge object to use + */ + public void acquireBridge(AndroidDebugBridge bridge) { + DeviceBridge.acquireBridge(bridge); + } + + /** + * Creates an {@link AndroidDebugBridge} connected to adb at the given location. + * + * If a bridge is already running, this disconnects it and creates a new one. + * + * @param adbLocation the location to adb. + */ + public void initDebugBridge() { + DeviceBridge.initDebugBridge(getAdbLocation()); + } + + public void stopDebugBridge() { + DeviceBridge.terminate(); + } + + public void populateDeviceSelectionModel() { + IDevice[] devices = DeviceBridge.getDevices(); + for (IDevice device : devices) { + deviceConnected(device); + } + } + + public void startListenForDevices() { + DeviceBridge.startListenForDevices(this); + } + + public void stopListenForDevices() { + DeviceBridge.stopListenForDevices(this); + } + + public abstract void executeInBackground(String taskName, Runnable task); + + @Override + public void deviceConnected(final IDevice device) { + executeInBackground("Connecting device", new Runnable() { + @Override + public void run() { + if (!device.isOnline()) { + return; + } + + IHvDevice hvDevice; + synchronized (mDevicesLock) { + hvDevice = mDevices.get(device); + if (hvDevice == null) { + hvDevice = HvDeviceFactory.create(device); + hvDevice.initializeViewDebug(); + hvDevice.addWindowChangeListener(getDirector()); + mDevices.put(device, hvDevice); + } else { + // attempt re-initializing view server if device state has changed + hvDevice.initializeViewDebug(); + } + } + + DeviceSelectionModel.getModel().addDevice(hvDevice); + focusChanged(device); + } + }); + } + + @Override + public void deviceDisconnected(final IDevice device) { + executeInBackground("Disconnecting device", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice; + synchronized (mDevicesLock) { + hvDevice = mDevices.get(device); + if (hvDevice != null) { + mDevices.remove(device); + } + } + + if (hvDevice == null) { + return; + } + + hvDevice.terminateViewDebug(); + hvDevice.removeWindowChangeListener(getDirector()); + DeviceSelectionModel.getModel().removeDevice(hvDevice); + if (PixelPerfectModel.getModel().getDevice() == device) { + PixelPerfectModel.getModel().setData(null, null, null); + } + Window treeViewWindow = TreeViewModel.getModel().getWindow(); + if (treeViewWindow != null && treeViewWindow.getDevice() == device) { + TreeViewModel.getModel().setData(null, null); + mFilterText = ""; //$NON-NLS-1$ + } + } + }); + } + + @Override + public void windowsChanged(final IDevice device) { + executeInBackground("Refreshing windows", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + hvDevice.reloadWindows(); + DeviceSelectionModel.getModel().updateDevice(hvDevice); + } + }); + } + + @Override + public void focusChanged(final IDevice device) { + executeInBackground("Updating focus", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + int focusedWindow = hvDevice.getFocusedWindow(); + DeviceSelectionModel.getModel().updateFocusedWindow(hvDevice, focusedWindow); + } + }); + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) { + deviceConnected(device); + } + } + + public void refreshPixelPerfect() { + final IDevice device = PixelPerfectModel.getModel().getDevice(); + if (device != null) { + // Some interesting logic here. We don't want to refresh the pixel + // perfect view 1000 times in a row if the focus keeps changing. We + // just + // want it to refresh following the last focus change. + boolean proceed = false; + synchronized (this) { + if (mPixelPerfectRefreshesInProgress <= 1) { + proceed = true; + mPixelPerfectRefreshesInProgress++; + } + } + if (proceed) { + executeInBackground("Refreshing pixel perfect screenshot", new Runnable() { + @Override + public void run() { + Image screenshotImage = getScreenshotImage(getHvDevice(device)); + if (screenshotImage != null) { + PixelPerfectModel.getModel().setImage(screenshotImage); + } + synchronized (HierarchyViewerDirector.this) { + mPixelPerfectRefreshesInProgress--; + } + } + + }); + } + } + } + + public void refreshPixelPerfectTree() { + final IDevice device = PixelPerfectModel.getModel().getDevice(); + if (device != null) { + executeInBackground("Refreshing pixel perfect tree", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + ViewNode viewNode = + hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); + if (viewNode != null) { + PixelPerfectModel.getModel().setTree(viewNode); + } + } + + }); + } + } + + public void loadPixelPerfectData(final IHvDevice hvDevice) { + executeInBackground("Loading pixel perfect data", new Runnable() { + @Override + public void run() { + Image screenshotImage = getScreenshotImage(hvDevice); + if (screenshotImage != null) { + ViewNode viewNode = + hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); + if (viewNode != null) { + PixelPerfectModel.getModel().setData(hvDevice.getDevice(), + screenshotImage, viewNode); + } + } + } + }); + } + + private IHvDevice getHvDevice(IDevice device) { + synchronized (mDevicesLock) { + return mDevices.get(device); + } + } + + private Image getScreenshotImage(IHvDevice hvDevice) { + return (hvDevice == null) ? null : hvDevice.getScreenshotImage(); + } + + public void loadViewTreeData(final Window window) { + executeInBackground("Loading view hierarchy", new Runnable() { + @Override + public void run() { + mFilterText = ""; //$NON-NLS-1$ + + IHvDevice hvDevice = window.getHvDevice(); + ViewNode viewNode = hvDevice.loadWindowData(window); + if (viewNode != null) { + viewNode.setViewCount(); + TreeViewModel.getModel().setData(window, viewNode); + } + } + }); + } + + public void loadOverlay(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + FileDialog fileDialog = new FileDialog(shell, SWT.OPEN); + fileDialog.setFilterExtensions(new String[] { + "*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)" + }); + fileDialog.setText("Choose an overlay image"); + String fileName = fileDialog.open(); + if (fileName != null) { + try { + Image image = new Image(Display.getDefault(), fileName); + PixelPerfectModel.getModel().setOverlayImage(image); + } catch (SWTException e) { + Log.e(TAG, "Unable to load image from " + fileName); + } + } + } + }); + } + + public void showCapture(final Shell shell, final ViewNode viewNode) { + executeInBackground("Capturing node", new Runnable() { + @Override + public void run() { + final Image image = loadCapture(viewNode); + if (image != null) { + + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + CaptureDisplay.show(shell, viewNode, image); + } + }); + } + } + }); + } + + public void showEvaluateContrast(final Shell shell) { + executeInBackground("Capturing node and evaluating contrast", new Runnable() { + @Override + public void run() { + mFilterText = ""; //$NON-NLS-1$ + Window window = TreeViewModel.getModel().getWindow(); + IHvDevice hvDevice = window.getHvDevice(); + final ViewNode viewNode = hvDevice.loadWindowData(window); + if (viewNode != null) { + viewNode.setViewCount(); + TreeViewModel.getModel().setData(window, viewNode); + } + + final Image image = loadCapture(viewNode); + if (image != null && viewNode != null) { + + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + EvaluateContrastDisplay.show(shell, viewNode, image); + } + }); + } + } + }); + } + + public void showDumpTheme(final Shell shell) { + executeInBackground("Capturing node and dumping theme", new Runnable() { + @Override + public void run() { + ViewNode viewNode; + Window window = TreeViewModel.getModel().getWindow(); + IHvDevice hvDevice = window.getHvDevice(); + + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree == null) { + viewNode = hvDevice.loadWindowData(window); + } else { + viewNode = tree.viewNode; + } + + final ThemeModel model = hvDevice.dumpTheme(viewNode); + if (model != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + DumpThemeDisplay.show(shell, model); + } + }); + } else { + Log.e(TAG, "Unable to dump theme."); + } + } + }); + } + + public Image loadCapture(ViewNode viewNode) { + IHvDevice hvDevice = viewNode.window.getHvDevice(); + final Image image = hvDevice.loadCapture(viewNode.window, viewNode); + if (image != null) { + viewNode.image = image; + + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + return image; + } + + public void loadCaptureInBackground(final ViewNode viewNode) { + executeInBackground("Capturing node", new Runnable() { + @Override + public void run() { + loadCapture(viewNode); + } + }); + } + + public void showCapture(Shell shell) { + DrawableViewNode viewNode = TreeViewModel.getModel().getSelection(); + if (viewNode != null) { + showCapture(shell, viewNode.viewNode); + } + } + + public void refreshWindows() { + executeInBackground("Refreshing windows", new Runnable() { + @Override + public void run() { + IHvDevice[] hvDevicesA = DeviceSelectionModel.getModel().getDevices(); + IDevice[] devicesA = new IDevice[hvDevicesA.length]; + for (int i = 0; i < hvDevicesA.length; i++) { + devicesA[i] = hvDevicesA[i].getDevice(); + } + IDevice[] devicesB = DeviceBridge.getDevices(); + HashSet deviceSet = new HashSet(); + for (int i = 0; i < devicesB.length; i++) { + deviceSet.add(devicesB[i]); + } + for (int i = 0; i < devicesA.length; i++) { + if (deviceSet.contains(devicesA[i])) { + windowsChanged(devicesA[i]); + deviceSet.remove(devicesA[i]); + } else { + deviceDisconnected(devicesA[i]); + } + } + for (IDevice device : deviceSet) { + deviceConnected(device); + } + } + }); + } + + public void loadViewHierarchy() { + Window window = DeviceSelectionModel.getModel().getSelectedWindow(); + if (window != null) { + loadViewTreeData(window); + } + } + + public void inspectScreenshot() { + IHvDevice device = DeviceSelectionModel.getModel().getSelectedDevice(); + if (device != null) { + loadPixelPerfectData(device); + } + } + + public void saveTreeView(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + final DrawableViewNode viewNode = TreeViewModel.getModel().getTree(); + if (viewNode != null) { + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Portable Network Graphics File (*.png)" + }); + fileDialog.setText("Choose where to save the tree image"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving tree view", new Runnable() { + @Override + public void run() { + Image image = TreeView.paintToImage(viewNode); + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + image.getImageData() + }; + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ + extensionedFileName += ".png"; //$NON-NLS-1$ + } + try { + imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); + } catch (SWTException e) { + Log.e(TAG, "Unable to save tree view as a PNG image at " + + fileName); + } + image.dispose(); + } + }); + } + } + } + }); + } + + public void savePixelPerfect(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image untouchableImage = PixelPerfectModel.getModel().getImage(); + if (untouchableImage != null) { + final ImageData imageData = untouchableImage.getImageData(); + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Portable Network Graphics File (*.png)" + }); + fileDialog.setText("Choose where to save the screenshot"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving pixel perfect", new Runnable() { + @Override + public void run() { + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + imageData + }; + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ + extensionedFileName += ".png"; //$NON-NLS-1$ + } + try { + imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); + } catch (SWTException e) { + Log.e(TAG, "Unable to save tree view as a PNG image at " + + fileName); + } + } + }); + } + } + } + }); + } + + public void capturePSD(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + final Window window = TreeViewModel.getModel().getWindow(); + if (window != null) { + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.psd" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Photoshop Document (*.psd)" + }); + fileDialog.setText("Choose where to save the window layers"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving window layers", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(window.getDevice()); + PsdFile psdFile = hvDevice.captureLayers(window); + if (psdFile != null) { + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$ + extensionedFileName += ".psd"; //$NON-NLS-1$ + } + try { + psdFile.write(new FileOutputStream(extensionedFileName)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Unable to write to file " + fileName); + } + } + } + }); + } + } + } + }); + } + + public void reloadViewHierarchy() { + Window window = TreeViewModel.getModel().getWindow(); + if (window != null) { + loadViewTreeData(window); + } + } + + public void invalidateCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Invalidating view", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.invalidateView(selectedNode.viewNode); + } + }); + } + } + + public void relayoutCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Request layout", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.requestLayout(selectedNode.viewNode); + } + }); + } + } + + public void dumpDisplayListForCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Dump displaylist", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.outputDisplayList(selectedNode.viewNode); + } + }); + } + } + + public void profileCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Profile Node", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.loadProfileData(selectedNode.viewNode.window, selectedNode.viewNode); + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + }); + } + } + + public void invokeMethodOnSelectedView(final String method, final List args) { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Invoke View Method", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.invokeViewMethod(selectedNode.viewNode.window, selectedNode.viewNode, + method, args); + } + }); + } + } + + public void loadAllViews() { + executeInBackground("Loading all views", new Runnable() { + @Override + public void run() { + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree != null) { + loadViewRecursive(tree.viewNode); + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + } + }); + } + + private void loadViewRecursive(ViewNode viewNode) { + IHvDevice hvDevice = getHvDevice(viewNode.window.getDevice()); + Image image = hvDevice.loadCapture(viewNode.window, viewNode); + if (image == null) { + return; + } + viewNode.image = image; + final int N = viewNode.children.size(); + for (int i = 0; i < N; i++) { + loadViewRecursive(viewNode.children.get(i)); + } + } + + public void filterNodes(String filterText) { + this.mFilterText = filterText; + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree != null) { + tree.viewNode.filter(filterText); + // Force redraw + TreeViewModel.getModel().notifySelectionChanged(); + } + } + + public String getFilterText() { + return mFilterText; + } + + private static class PixelPerfectAutoRefreshTask extends TimerTask { + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfect(); + } + }; + + public void setPixelPerfectAutoRefresh(boolean value) { + synchronized (mPixelPerfectRefreshTimer) { + if (value == mAutoRefresh) { + return; + } + mAutoRefresh = value; + if (mAutoRefresh) { + mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); + mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, + mPixelPerfectAutoRefreshInterval * 1000, + mPixelPerfectAutoRefreshInterval * 1000); + } else { + mCurrentAutoRefreshTask.cancel(); + mCurrentAutoRefreshTask = null; + } + } + } + + public void setPixelPerfectAutoRefreshInterval(int value) { + synchronized (mPixelPerfectRefreshTimer) { + if (mPixelPerfectAutoRefreshInterval == value) { + return; + } + mPixelPerfectAutoRefreshInterval = value; + if (mAutoRefresh) { + mCurrentAutoRefreshTask.cancel(); + long timeLeft = + Math.max(0, mPixelPerfectAutoRefreshInterval + * 1000 + - (System.currentTimeMillis() - mCurrentAutoRefreshTask + .scheduledExecutionTime())); + mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); + mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft, + mPixelPerfectAutoRefreshInterval * 1000); + } + } + } + + public int getPixelPerfectAutoRefreshInverval() { + return mPixelPerfectAutoRefreshInterval; + } + + public ImageFactory getImageFactory() { + return mImageFactory; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java new file mode 100644 index 00000000..cc74a096 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class CapturePSDAction extends TreeViewEnabledAction implements ImageAction { + + private static CapturePSDAction sAction; + + private Image mImage; + + private Shell mShell; + + private CapturePSDAction(Shell shell) { + super("&Capture Layers"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'C'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("capture-psd.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Capture the window layers as a photoshop document"); + } + + public static CapturePSDAction getAction(Shell shell) { + if (sAction == null) { + sAction = new CapturePSDAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().capturePSD(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java new file mode 100644 index 00000000..c020e813 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class DisplayViewAction extends SelectedNodeEnabledAction implements ImageAction { + + private static DisplayViewAction sAction; + + private Image mImage; + + private Shell mShell; + + private DisplayViewAction(Shell shell) { + super("&Display View"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("display.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Display the selected view image in a separate window"); + } + + public static DisplayViewAction getAction(Shell shell) { + if (sAction == null) { + sAction = new DisplayViewAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showCapture(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java new file mode 100644 index 00000000..74372c88 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +public class DumpDisplayListAction extends SelectedNodeEnabledAction implements ImageAction { + + private static DumpDisplayListAction sAction; + + private Image mImage; + + private DumpDisplayListAction() { + super("Dump DisplayList"); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Request the view to output its displaylist to logcat"); + } + + public static DumpDisplayListAction getAction() { + if (sAction == null) { + sAction = new DumpDisplayListAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().dumpDisplayListForCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java new file mode 100644 index 00000000..9465cf98 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class DumpThemeAction extends Action implements ImageAction { + + private static DumpThemeAction sAction; + + private Image mImage; + + private Shell mShell; + + private DumpThemeAction(Shell shell) { + super("&Dump Theme"); + mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + setToolTipText("Dumping the resources in this View's Theme."); + // TODO: Get icon for Button + } + + public static DumpThemeAction getAction(Shell shell) { + if (sAction == null) { + sAction = new DumpThemeAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showDumpTheme(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java new file mode 100644 index 00000000..ad80a97b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class EvaluateContrastAction extends Action implements ImageAction { + + private static EvaluateContrastAction sAction; + + private Image mImage; + + private Shell mShell; + + private EvaluateContrastAction(Shell shell) { + super("&Evaluate Contrast"); + mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + setToolTipText("Evaluate the contrast ratio of this view."); + // TODO: Get icon for Button + } + + public static EvaluateContrastAction getAction(Shell shell) { + if (sAction == null) { + sAction = new EvaluateContrastAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showEvaluateContrast(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java new file mode 100644 index 00000000..6aa2858b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import org.eclipse.swt.graphics.Image; + +public interface ImageAction { + public Image getImage(); + + public String getText(); + + public String getToolTipText(); +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java new file mode 100644 index 00000000..71f857c2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class InspectScreenshotAction extends Action implements ImageAction, IWindowChangeListener { + + private static InspectScreenshotAction sAction; + + private Image mImage; + + private InspectScreenshotAction() { + super("Inspect &Screenshot"); + setAccelerator(SWT.MOD1 + 'S'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("inspect-screenshot.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Inspect a screenshot in the pixel perfect view"); + setEnabled( + DeviceSelectionModel.getModel().getSelectedDevice() != null); + DeviceSelectionModel.getModel().addWindowChangeListener(this); + } + + public static InspectScreenshotAction getAction() { + if (sAction == null) { + sAction = new InspectScreenshotAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().inspectScreenshot(); + } + + @Override + public Image getImage() { + return mImage; + } + + @Override + public void deviceChanged(IHvDevice device) { + // pass + } + + @Override + public void deviceConnected(IHvDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IHvDevice device) { + // pass + } + + @Override + public void focusChanged(IHvDevice device) { + // pass + } + + @Override + public void selectionChanged(final IHvDevice device, final Window window) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + InspectScreenshotAction.getAction().setEnabled(device != null); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java new file mode 100644 index 00000000..a6e45552 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class InvalidateAction extends SelectedNodeEnabledAction implements ImageAction { + + private static InvalidateAction sAction; + + private Image mImage; + + private InvalidateAction() { + super("&Invalidate Layout"); + setAccelerator(SWT.MOD1 + 'I'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("invalidate.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Invalidate the layout for the current window"); + } + + public static InvalidateAction getAction() { + if (sAction == null) { + sAction = new InvalidateAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().invalidateCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java new file mode 100644 index 00000000..1ced3c7f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class LoadOverlayAction extends PixelPerfectEnabledAction implements ImageAction { + + private static LoadOverlayAction sAction; + + private Image mImage; + + private Shell mShell; + + private LoadOverlayAction(Shell shell) { + super("Load &Overlay"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'O'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-overlay.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Load an image to overlay the screenshot"); + } + + public static LoadOverlayAction getAction(Shell shell) { + if (sAction == null) { + sAction = new LoadOverlayAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().loadOverlay(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java new file mode 100644 index 00000000..b1384d2d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class LoadViewHierarchyAction extends Action implements ImageAction, IWindowChangeListener { + + private static LoadViewHierarchyAction sAction; + + private Image mImage; + + private LoadViewHierarchyAction() { + super("Load View &Hierarchy"); + setAccelerator(SWT.MOD1 + 'H'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Load the view hierarchy into the tree view"); + setEnabled( + DeviceSelectionModel.getModel().getSelectedWindow() != null); + DeviceSelectionModel.getModel().addWindowChangeListener(this); + } + + public static LoadViewHierarchyAction getAction() { + if (sAction == null) { + sAction = new LoadViewHierarchyAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().loadViewHierarchy(); + } + + @Override + public Image getImage() { + return mImage; + } + + @Override + public void deviceChanged(IHvDevice device) { + // pass + } + + @Override + public void deviceConnected(IHvDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IHvDevice device) { + // pass + } + + @Override + public void focusChanged(IHvDevice device) { + // pass + } + + @Override + public void selectionChanged(final IHvDevice device, final Window window) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + LoadViewHierarchyAction.getAction().setEnabled(window != null); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java new file mode 100644 index 00000000..cd8f36d2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction implements ImageAction { + + private static PixelPerfectAutoRefreshAction sAction; + + private Image mImage; + + private PixelPerfectAutoRefreshAction() { + super("Auto &Refresh", Action.AS_CHECK_BOX); + setAccelerator(SWT.MOD1 + 'R'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("auto-refresh.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Automatically refresh the screenshot"); + } + + public static PixelPerfectAutoRefreshAction getAction() { + if (sAction == null) { + sAction = new PixelPerfectAutoRefreshAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefresh(sAction.isChecked()); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java new file mode 100644 index 00000000..b5d22345 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectEnabledAction extends Action implements IImageChangeListener { + public PixelPerfectEnabledAction(String name) { + super(name); + setEnabled(PixelPerfectModel.getModel().getImage() != null); + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + public PixelPerfectEnabledAction(String name, int type) { + super(name, type); + setEnabled(PixelPerfectModel.getModel().getImage() != null); + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void imageChanged() { + // + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(PixelPerfectModel.getModel().getImage() != null); + } + }); + } + + @Override + public void overlayChanged() { + // pass + } + + @Override + public void overlayTransparencyChanged() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java new file mode 100644 index 00000000..8e75d3f2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; + +public class ProfileNodesAction extends SelectedNodeEnabledAction implements ImageAction { + private static ProfileNodesAction sAction; + + private Image mImage; + + public ProfileNodesAction() { + super("Profile Node"); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("profile.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Obtain layout times for tree rooted at selected node"); + } + + public static ProfileNodesAction getAction() { + if (sAction == null) { + sAction = new ProfileNodesAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().profileCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java new file mode 100644 index 00000000..c2edede8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { + + private static RefreshPixelPerfectAction sAction; + + private Image mImage; + + private RefreshPixelPerfectAction() { + super("&Refresh Screenshot"); + setAccelerator(SWT.F5); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("refresh-windows.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the screenshot"); + } + + public static RefreshPixelPerfectAction getAction() { + if (sAction == null) { + sAction = new RefreshPixelPerfectAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfect(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java new file mode 100644 index 00000000..bb1d5b18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction implements ImageAction { + + private static RefreshPixelPerfectTreeAction sAction; + + private Image mImage; + + private RefreshPixelPerfectTreeAction() { + super("Refresh &Tree"); + setAccelerator(SWT.MOD1 + 'T'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the tree"); + } + + public static RefreshPixelPerfectTreeAction getAction() { + if (sAction == null) { + sAction = new RefreshPixelPerfectTreeAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfectTree(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java new file mode 100644 index 00000000..2eb65783 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class RefreshViewAction extends TreeViewEnabledAction implements ImageAction { + + private static RefreshViewAction sAction; + + private Image mImage; + + private RefreshViewAction() { + super("Load View &Hierarchy"); + setAccelerator(SWT.MOD1 + 'H'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Reload the view hierarchy"); + } + + public static RefreshViewAction getAction() { + if (sAction == null) { + sAction = new RefreshViewAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().reloadViewHierarchy(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java new file mode 100644 index 00000000..82c5237a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class RefreshWindowsAction extends Action implements ImageAction { + + private static RefreshWindowsAction sAction; + + private Image mImage; + + private RefreshWindowsAction() { + super("&Refresh"); + setAccelerator(SWT.F5); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("refresh-windows.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the list of devices"); + } + + public static RefreshWindowsAction getAction() { + if (sAction == null) { + sAction = new RefreshWindowsAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshWindows(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java new file mode 100644 index 00000000..a15da7fd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; + +public class RequestLayoutAction extends SelectedNodeEnabledAction implements ImageAction { + + private static RequestLayoutAction sAction; + + private Image mImage; + + private RequestLayoutAction() { + super("Request &Layout"); + setAccelerator(SWT.MOD1 + 'L'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("request-layout.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Request the view to lay out"); + } + + public static RequestLayoutAction getAction() { + if (sAction == null) { + sAction = new RequestLayoutAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().relayoutCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java new file mode 100644 index 00000000..1bcdc450 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class SavePixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { + + private static SavePixelPerfectAction sAction; + + private Image mImage; + + private Shell mShell; + + private SavePixelPerfectAction(Shell shell) { + super("&Save as PNG"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'S'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("save.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Save the screenshot as a PNG image"); + } + + public static SavePixelPerfectAction getAction(Shell shell) { + if (sAction == null) { + sAction = new SavePixelPerfectAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().savePixelPerfect(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java new file mode 100644 index 00000000..c8c3a036 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class SaveTreeViewAction extends TreeViewEnabledAction implements ImageAction { + + private static SaveTreeViewAction sAction; + + private Image mImage; + + private Shell mShell; + + private SaveTreeViewAction(Shell shell) { + super("&Save as PNG"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'S'); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("save.png"); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Save the tree view as a PNG image"); + } + + public static SaveTreeViewAction getAction(Shell shell) { + if (sAction == null) { + sAction = new SaveTreeViewAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().saveTreeView(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java new file mode 100644 index 00000000..80471d27 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class SelectedNodeEnabledAction extends Action implements ITreeChangeListener { + public SelectedNodeEnabledAction(String name) { + super(name); + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + TreeViewModel.getModel().addTreeChangeListener(this); + } + + @Override + public void selectionChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + } + }); + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + } + }); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java new file mode 100644 index 00000000..f2cb57b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class TreeViewEnabledAction extends Action implements ITreeChangeListener { + public TreeViewEnabledAction(String name) { + super(name); + setEnabled(TreeViewModel.getModel().getTree() != null); + TreeViewModel.getModel().addTreeChangeListener(this); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null); + } + }); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java new file mode 100644 index 00000000..495b3d37 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.device; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.TimeoutException; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.widgets.Display; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class AbstractHvDevice implements IHvDevice { + private static final String TAG = "HierarchyViewer"; + + @Override + public Image getScreenshotImage() { + IDevice device = getDevice(); + final AtomicReference imageRef = new AtomicReference(); + + try { + final RawImage screenshot = device.getScreenshot(); + if (screenshot == null) { + return null; + } + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + ImageData imageData = + new ImageData(screenshot.width, screenshot.height, screenshot.bpp, + new PaletteData(screenshot.getRedMask(), screenshot + .getGreenMask(), screenshot.getBlueMask()), 1, + screenshot.data); + imageRef.set(new Image(Display.getDefault(), imageData)); + } + }); + return imageRef.get(); + } catch (IOException e) { + Log.e(TAG, "Unable to load screenshot from device " + device.getName()); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout loading screenshot from device " + device.getName()); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, "Adb rejected command to load screenshot from device " + device.getName()); + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java new file mode 100644 index 00000000..01e09739 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.device; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.HandleViewDebug; +import com.android.ddmlib.HandleViewDebug.ViewDumpHandler; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class DdmViewDebugDevice extends AbstractHvDevice implements IDeviceChangeListener { + private static final String TAG = "DdmViewDebugDevice"; + + private final IDevice mDevice; + private Map> mViewRootsPerClient = new HashMap>(40); + + public DdmViewDebugDevice(IDevice device) { + mDevice = device; + } + + @Override + public boolean initializeViewDebug() { + AndroidDebugBridge.addDeviceChangeListener(this); + return reloadWindows(); + } + + private static class ListViewRootsHandler extends ViewDumpHandler { + private List mViewRoots = Collections.synchronizedList(new ArrayList(10)); + + public ListViewRootsHandler() { + super(HandleViewDebug.CHUNK_VULW); + } + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + int nWindows = data.getInt(); + + for (int i = 0; i < nWindows; i++) { + int len = data.getInt(); + mViewRoots.add(getString(data, len)); + } + } + + public List getViewRoots(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mViewRoots; + } + } + + private static class CaptureByteArrayHandler extends ViewDumpHandler { + public CaptureByteArrayHandler(int type) { + super(type); + } + + private AtomicReference mData = new AtomicReference(); + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + byte[] b = new byte[data.remaining()]; + data.get(b); + mData.set(b); + + } + + public byte[] getData(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mData.get(); + } + } + + private static class CaptureLayersHandler extends ViewDumpHandler { + private AtomicReference mPsd = new AtomicReference(); + + public CaptureLayersHandler() { + super(HandleViewDebug.CHUNK_VURT); + } + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + byte[] b = new byte[data.remaining()]; + data.get(b); + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(b)); + try { + mPsd.set(DeviceBridge.parsePsd(dis)); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + public PsdFile getPsdFile(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mPsd.get(); + } + } + + @Override + public boolean reloadWindows() { + mViewRootsPerClient = new HashMap>(40); + + for (Client c : mDevice.getClients()) { + ClientData cd = c.getClientData(); + if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) { + ListViewRootsHandler handler = new ListViewRootsHandler(); + + try { + HandleViewDebug.listViewRoots(c, handler); + } catch (IOException e) { + Log.i(TAG, "No connection to client: " + cd.getClientDescription()); + continue; + } + + List viewRoots = new ArrayList( + handler.getViewRoots(200, TimeUnit.MILLISECONDS)); + mViewRootsPerClient.put(c, viewRoots); + } + } + + return true; + } + + @Override + public void terminateViewDebug() { + // nothing to terminate + } + + @Override + public boolean isViewDebugEnabled() { + return true; + } + + @Override + public boolean supportsDisplayListDump() { + return true; + } + + @Override + public Window[] getWindows() { + List windows = new ArrayList(10); + + for (Client c: mViewRootsPerClient.keySet()) { + for (String viewRoot: mViewRootsPerClient.get(c)) { + windows.add(new Window(this, viewRoot, c)); + } + } + + return windows.toArray(new Window[windows.size()]); + } + + @Override + public int getFocusedWindow() { + // TODO: add support for identifying view in focus + return -1; + } + + @Override + public IDevice getDevice() { + return mDevice; + } + + @Override + public ViewNode loadWindowData(Window window) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); + try { + HandleViewDebug.dumpViewHierarchy(c, viewRoot, + false /* skipChildren */, + true /* includeProperties */, + handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(20, TimeUnit.SECONDS); + if (data == null) { + return null; + } + + String viewHierarchy = new String(data, Charset.forName("UTF-8")); + return DeviceBridge.parseViewHierarchy(new BufferedReader(new StringReader(viewHierarchy)), + window); + } + + @Override + public void loadProfileData(Window window, ViewNode viewNode) { + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); + try { + HandleViewDebug.profileView(c, viewRoot, viewNode.toString(), handler); + } catch (IOException e) { + Log.e(TAG, e); + return; + } + + byte[] data = handler.getData(30, TimeUnit.SECONDS); + if (data == null) { + Log.e(TAG, "Timed out waiting for profile data"); + return; + } + + try { + boolean success = DeviceBridge.loadProfileDataRecursive(viewNode, + new BufferedReader(new StringReader(new String(data)))); + if (success) { + viewNode.setProfileRatings(); + } + } catch (IOException e) { + Log.e(TAG, e); + return; + } + } + + @Override + public Image loadCapture(Window window, ViewNode viewNode) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); + + try { + HandleViewDebug.captureView(c, viewRoot, viewNode.toString(), handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(10, TimeUnit.SECONDS); + return (data == null) ? null : + new Image(Display.getDefault(), new ByteArrayInputStream(data)); + } + + @Override + public PsdFile captureLayers(Window window) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureLayersHandler handler = new CaptureLayersHandler(); + try { + HandleViewDebug.captureLayers(c, viewRoot, handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + return handler.getPsdFile(20, TimeUnit.SECONDS); + } + + @Override + public void invalidateView(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.invalidateView(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public void requestLayout(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.requestLayout(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public void outputDisplayList(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.dumpDisplayList(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public ThemeModel dumpTheme(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); + try { + HandleViewDebug.dumpTheme(c, viewRoot, handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(20, TimeUnit.SECONDS); + if (data == null) { + return null; + } + + String themeDump = new String(data, Charset.forName("UTF-8")); + return DeviceBridge.parseThemeDump(new BufferedReader(new StringReader(themeDump))); + } + + @Override + public void addWindowChangeListener(IWindowChangeListener l) { + // TODO: add support for listening to view root changes + } + + @Override + public void removeWindowChangeListener(IWindowChangeListener l) { + // TODO: add support for listening to view root changes + } + + @Override + public void deviceConnected(IDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IDevice device) { + // pass + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + if ((changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) { + reloadWindows(); + } + } + + @Override + public boolean isViewUpdateEnabled() { + return true; + } + + @Override + public void invokeViewMethod(Window window, ViewNode viewNode, String method, + List args) { + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.invokeMethod(c, viewRoot, viewNode.toString(), method, args.toArray()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public boolean setLayoutParameter(Window window, ViewNode viewNode, String property, + int value) { + Client c = window.getClient(); + if (c == null) { + return false; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.setLayoutParameter(c, viewRoot, viewNode.toString(), property, value); + } catch (IOException e) { + Log.e(TAG, e); + return false; + } + + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java new file mode 100644 index 00000000..0deed842 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java @@ -0,0 +1,744 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.device; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +/** + * A bridge to the device. + */ +public class DeviceBridge { + + public static final String TAG = "hierarchyviewer"; + + private static final int DEFAULT_SERVER_PORT = 4939; + + // These codes must match the auto-generated codes in IWindowManager.java + // See IWindowManager.aidl as well + private static final int SERVICE_CODE_START_SERVER = 1; + + private static final int SERVICE_CODE_STOP_SERVER = 2; + + private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3; + + private static AndroidDebugBridge sBridge; + + private static final HashMap sDevicePortMap = new HashMap(); + + private static final HashMap sViewServerInfo = + new HashMap(); + + private static int sNextLocalPort = DEFAULT_SERVER_PORT; + + public static class ViewServerInfo { + public final int protocolVersion; + + public final int serverVersion; + + ViewServerInfo(int serverVersion, int protocolVersion) { + this.protocolVersion = protocolVersion; + this.serverVersion = serverVersion; + } + } + + /** + * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. + * @param bridge the bridge object to use + */ + public static void acquireBridge(AndroidDebugBridge bridge) { + sBridge = bridge; + } + + /** + * Creates an {@link AndroidDebugBridge} connected to adb at the given location. + * + * If a bridge is already running, this disconnects it and creates a new one. + * + * @param adbLocation the location to adb. + */ + public static void initDebugBridge(String adbLocation) { + if (sBridge == null) { + /* debugger support required only if hv is using ddm protocol */ + AndroidDebugBridge.init(HierarchyViewerDirector.isUsingDdmProtocol()); + } + if (sBridge == null || !sBridge.isConnected()) { + sBridge = AndroidDebugBridge.createBridge(adbLocation, true); + } + } + + /** Disconnects the current {@link AndroidDebugBridge}. */ + public static void terminate() { + AndroidDebugBridge.terminate(); + } + + public static IDevice[] getDevices() { + if (sBridge == null) { + return new IDevice[0]; + } + return sBridge.getDevices(); + } + + /* + * This adds a listener to the debug bridge. The listener is notified of + * connecting/disconnecting devices, devices coming online, etc. + */ + public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { + AndroidDebugBridge.addDeviceChangeListener(listener); + } + + public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { + AndroidDebugBridge.removeDeviceChangeListener(listener); + } + + /** + * Sets up a just-connected device to work with the view server. + *

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

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

+ *

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

+ *

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

+ *

+ *

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

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

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

+ *

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

+ * + * @param image Screenshot of the view. + * @param textColor Color of the text. If null, we will estimate the color of the text. + * @param textSize Size of the text. If null, we may have an indeterminate result where it + * passes only one of the tests. + * @param x Starting x-coordinate of the view in the image. + * @param y Starting y-coordinate of the view in the image. + * @param width The width of the view in the image. + * @param height The height of the view in the image. + * @param isBold True if we know the text is bold, false otherwise. + */ + public EvaluateContrastModel(Image image, @Nullable Integer textColor, + @Nullable Double textSize, int x, int y, int width, int height, boolean isBold) { + mImageData = image.getImageData(); + mTextColor = textColor; + mTextSize = textSize; + mViewBounds = new Rectangle(x, y, width, height); + mIsBold = isBold; + + mBackgroundColors = new LinkedList(); + mForegroundColors = new LinkedList(); + mLuminanceMap = new HashMap(); + mLuminanceHistogram = new HashMap(); + + processSwatch(); + } + + /** + * Formula derived from http://gmazzocato.altervista.org/colorwheel/algo.php. + * More information can be found at http://www.w3.org/TR/WCAG20/relative-luminance.xml. + */ + public static double calculateLuminance(int color) { + Color colorObj = new Color(color); + float[] sRGB = new float[4]; + colorObj.getRGBComponents(sRGB); + + final double[] lumRGB = new double[4]; + for (int i = 0; i < sRGB.length; ++i) { + lumRGB[i] = (sRGB[i] <= 0.03928d) ? sRGB[i] / 12.92d + : Math.pow(((sRGB[i] + 0.055d) / 1.055d), 2.4d); + } + + return 0.2126d * lumRGB[0] + 0.7152d * lumRGB[1] + 0.0722d * lumRGB[2]; + } + + public static double calculateContrastRatio(double lum1, double lum2) { + if ((lum1 < 0.0d) || (lum2 < 0.0d)) { + throw new IllegalArgumentException("Luminance values may not be negative."); + } + + return (Math.max(lum1, lum2) + 0.05d) / (Math.min(lum1, lum2) + 0.05d); + } + + public static String intToHexString(int color) { + return String.format("#%06X", (0xFFFFFF & color)); + } + + private void processSwatch() { + processLuminanceData(); + extractFgBgData(); + + double textLuminance = mTextColor == null ? mForegroundLuminance : + calculateLuminance(calculateTextColor(mTextColor, mBackgroundColors.get(0))); + // Two-decimal digits of precision for the contrast ratio + mContrastRatio = Math.round(calculateContrastRatio( + textLuminance, mBackgroundLuminance) * 100.0d) / 100.0d; + } + + private void processLuminanceData() { + for (int x = mViewBounds.x; x < mViewBounds.width; ++x) { + for (int y = mViewBounds.y; y < mViewBounds.height; ++y) { + final int color = mImageData.getPixel(x, y); + final double luminance = calculateLuminance(color); + if (!mLuminanceMap.containsKey(color)) { + mLuminanceMap.put(color, luminance); + } + + if (!mLuminanceHistogram.containsKey(luminance)) { + mLuminanceHistogram.put(luminance, 0); + } + + mLuminanceHistogram.put(luminance, mLuminanceHistogram.get(luminance) + 1); + } + } + } + + private void extractFgBgData() { + if (mLuminanceMap.isEmpty()) { + // An empty luminance map indicates we've encountered a 0px area + // image. It has no luminance. + mBackgroundLuminance = mForegroundLuminance = 0; + mBackgroundColors.add(0); + mForegroundColors.add(0); + } else if (mLuminanceMap.size() == 1) { + // Deal with views that only contain a single color + mBackgroundLuminance = mForegroundLuminance = mLuminanceHistogram.keySet().iterator() + .next(); + final int singleColor = mLuminanceMap.keySet().iterator().next(); + mForegroundColors.add(singleColor); + mBackgroundColors.add(singleColor); + } else { + // Sort all luminance values seen from low to high + final ArrayList> colorsByLuminance = + Lists.newArrayList(mLuminanceMap.entrySet()); + Collections.sort(colorsByLuminance, new Comparator>() { + @Override + public int compare(Entry lhs, Entry rhs) { + return Double.compare(lhs.getValue(), rhs.getValue()); + } + }); + + // Sort luminance values seen by frequency in the image + final ArrayList> luminanceByFrequency = + Lists.newArrayList(mLuminanceHistogram.entrySet()); + Collections.sort(luminanceByFrequency, new Comparator>() { + @Override + public int compare(Entry lhs, Entry rhs) { + return lhs.getValue() - rhs.getValue(); + } + }); + + // Find the average luminance value within the set of luminances for + // purposes of splitting luminance values into high-luminance and + // low-luminance buckets. This is explicitly not a weighted average. + double luminanceSum = 0; + for (Entry luminanceCount : luminanceByFrequency) { + luminanceSum += luminanceCount.getKey(); + } + + final double averageLuminance = luminanceSum / luminanceByFrequency.size(); + + // Select the highest and lowest luminance values that contribute to + // most number of pixels in the image -- our background and + // foreground colors. + double lowLuminanceContributor = 0.0d; + for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { + final double luminanceValue = luminanceByFrequency.get(i).getKey(); + if (luminanceValue < averageLuminance) { + lowLuminanceContributor = luminanceValue; + break; + } + } + + double highLuminanceContributor = 1.0d; + for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { + final double luminanceValue = luminanceByFrequency.get(i).getKey(); + if (luminanceValue >= averageLuminance) { + highLuminanceContributor = luminanceValue; + break; + } + } + + // Background luminance is that which occurs more frequently + if (mLuminanceHistogram.get(highLuminanceContributor) + > mLuminanceHistogram.get(lowLuminanceContributor)) { + mBackgroundLuminance = highLuminanceContributor; + mForegroundLuminance = lowLuminanceContributor; + } else { + mBackgroundLuminance = lowLuminanceContributor; + mForegroundLuminance = highLuminanceContributor; + } + + // Determine the contributing colors for those luminance values + // TODO: Optimize (find an alternative to reiterating through whole image) + for (Entry colorLuminance : mLuminanceMap.entrySet()) { + if (colorLuminance.getValue() == mBackgroundLuminance) { + mBackgroundColors.add(colorLuminance.getKey()); + } + + if (colorLuminance.getValue() == mForegroundLuminance) { + mForegroundColors.add(colorLuminance.getKey()); + } + } + } + } + + /** + * Calculates a more accurate text color for how the text in the view appears by using its alpha + * value to determine how much it needs to be blended into its background color. + * + * @param textColor Text color. + * @param backgroundColor Background color. + * @return Calculated text color. + */ + private int calculateTextColor(int textColor, int backgroundColor) { + Color text = new Color(textColor, true); + Color background = new Color(backgroundColor, true); + + int alpha = text.getAlpha(); + double alphaPercentage = alpha / MAX_RGB_VALUE; + double alphaCompliment = 1 - alphaPercentage; + + int red = (int) (alphaPercentage * text.getRed() + alphaCompliment * background.getRed()); + int green = (int) (alphaPercentage * text.getGreen() + + alphaCompliment * background.getGreen()); + int blue = (int) (alphaPercentage * text.getBlue() + + alphaCompliment * background.getBlue()); + + Color rgb = new Color(red, green, blue, (int) MAX_RGB_VALUE); + mTextColor = rgb.getRGB(); + + return mTextColor; + } + + public ContrastResult getContrastResult() { + ContrastResult normalTest = getContrastResultForNormalText(); + ContrastResult largeTest = getContrastResultForLargeText(); + + if (normalTest == largeTest) { + return normalTest; + } else if (mTextSize == null) { + return ContrastResult.INDETERMINATE; + } else if (mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS && mIsBold || + mTextSize > NORMAL_TEXT_SZ_PTS) { + return largeTest; + } else { + return normalTest; + } + } + + public ContrastResult getContrastResultForLargeText() { + return mContrastRatio >= CONTRAST_RATIO_LARGE_TEXT ? + ContrastResult.PASS : ContrastResult.FAIL; + } + + public ContrastResult getContrastResultForNormalText() { + if (mIsBold && mTextSize != null && mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS) { + return getContrastResultForLargeText(); + } + return mContrastRatio >= CONTRAST_RATIO_NORMAL_TEXT ? + ContrastResult.PASS : ContrastResult.FAIL; + } + + public double getContrastRatio() { + return mContrastRatio; + } + + public double getBackgroundLuminance() { + return mBackgroundLuminance; + } + + public String getTextSize() { + if (mTextSize == null ){ + return NOT_APPLICABLE; + } + return Double.toString(mTextSize); + } + + public int getTextColor() { + Integer textColor; + + if (mTextColor != null) { + textColor = mTextColor; + } else { + // assumes that the foreground color is the luminance value that occurs the least + // frequently; which is also the best estimate we have for text color. + textColor = mForegroundColors.get(0); + } + + return textColor.intValue(); + } + + public String getTextColorHex() { + return intToHexString(getTextColor()); + } + + public int getBackgroundColor() { + return mBackgroundColors.get(0); + } + + public String getBackgroundColorHex() { + return intToHexString(mBackgroundColors.get(0)); + } + + public boolean isIndeterminate() { + return mTextSize == null && getContrastResult() == ContrastResult.INDETERMINATE; + } + + public boolean isBold() { + return mIsBold; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java new file mode 100644 index 00000000..f054b0f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import com.android.ddmlib.IDevice; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Display; + +import java.util.ArrayList; + +public class PixelPerfectModel { + + public static final int MIN_ZOOM = 2; + + public static final int MAX_ZOOM = 24; + + public static final int DEFAULT_ZOOM = 8; + + public static final int DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE = 50; + + private IDevice mDevice; + + private Image mImage; + + private Point mCrosshairLocation; + + private ViewNode mViewNode; + + private ViewNode mSelectedNode; + + private int mZoom; + + private final ArrayList mImageChangeListeners = + new ArrayList(); + + private Image mOverlayImage; + + private double mOverlayTransparency = DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE / 100.0; + + private static PixelPerfectModel sModel; + + public static PixelPerfectModel getModel() { + if (sModel == null) { + sModel = new PixelPerfectModel(); + } + return sModel; + } + + public void setData(final IDevice device, final Image image, final ViewNode viewNode) { + final Image toDispose = this.mImage; + final Image toDispose2 = this.mOverlayImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mDevice = device; + PixelPerfectModel.this.mImage = image; + PixelPerfectModel.this.mViewNode = viewNode; + if (image != null) { + PixelPerfectModel.this.mCrosshairLocation = + new Point(image.getBounds().width / 2, image.getBounds().height / 2); + } else { + PixelPerfectModel.this.mCrosshairLocation = null; + } + mOverlayImage = null; + PixelPerfectModel.this.mSelectedNode = null; + mZoom = DEFAULT_ZOOM; + } + } + }); + notifyImageLoaded(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + if (toDispose2 != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose2.dispose(); + } + }); + } + + } + + public void setCrosshairLocation(int x, int y) { + synchronized (this) { + mCrosshairLocation = new Point(x, y); + } + notifyCrosshairMoved(); + } + + public void setSelected(ViewNode selected) { + synchronized (this) { + this.mSelectedNode = selected; + } + notifySelectionChanged(); + } + + public void setTree(final ViewNode viewNode) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mViewNode = viewNode; + PixelPerfectModel.this.mSelectedNode = null; + } + } + }); + notifyTreeChanged(); + } + + public void setImage(final Image image) { + final Image toDispose = this.mImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mImage = image; + } + } + }); + notifyImageChanged(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + } + + public void setZoom(int newZoom) { + synchronized (this) { + if (newZoom < MIN_ZOOM) { + newZoom = MIN_ZOOM; + } + if (newZoom > MAX_ZOOM) { + newZoom = MAX_ZOOM; + } + mZoom = newZoom; + } + notifyZoomChanged(); + } + + public void setOverlayImage(final Image overlayImage) { + final Image toDispose = this.mOverlayImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mOverlayImage = overlayImage; + } + } + }); + notifyOverlayChanged(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + } + + public void setOverlayTransparency(double value) { + synchronized (this) { + value = Math.max(value, 0); + value = Math.min(value, 1); + mOverlayTransparency = value; + } + notifyOverlayTransparencyChanged(); + } + + public ViewNode getViewNode() { + synchronized (this) { + return mViewNode; + } + } + + public Point getCrosshairLocation() { + synchronized (this) { + return mCrosshairLocation; + } + } + + public Image getImage() { + synchronized (this) { + return mImage; + } + } + + public ViewNode getSelected() { + synchronized (this) { + return mSelectedNode; + } + } + + public IDevice getDevice() { + synchronized (this) { + return mDevice; + } + } + + public int getZoom() { + synchronized (this) { + return mZoom; + } + } + + public Image getOverlayImage() { + synchronized (this) { + return mOverlayImage; + } + } + + public double getOverlayTransparency() { + synchronized (this) { + return mOverlayTransparency; + } + } + + public static interface IImageChangeListener { + public void imageLoaded(); + + public void imageChanged(); + + public void crosshairMoved(); + + public void selectionChanged(); + + public void treeChanged(); + + public void zoomChanged(); + + public void overlayChanged(); + + public void overlayTransparencyChanged(); + } + + private IImageChangeListener[] getImageChangeListenerList() { + IImageChangeListener[] listeners = null; + synchronized (mImageChangeListeners) { + if (mImageChangeListeners.size() == 0) { + return null; + } + listeners = + mImageChangeListeners.toArray(new IImageChangeListener[mImageChangeListeners + .size()]); + } + return listeners; + } + + public void notifyImageLoaded() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].imageLoaded(); + } + } + } + + public void notifyImageChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].imageChanged(); + } + } + } + + public void notifyCrosshairMoved() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].crosshairMoved(); + } + } + } + + public void notifySelectionChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].selectionChanged(); + } + } + } + + public void notifyTreeChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].treeChanged(); + } + } + } + + public void notifyZoomChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].zoomChanged(); + } + } + } + + public void notifyOverlayChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].overlayChanged(); + } + } + } + + public void notifyOverlayTransparencyChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].overlayTransparencyChanged(); + } + } + } + + public void addImageChangeListener(IImageChangeListener listener) { + synchronized (mImageChangeListeners) { + mImageChangeListeners.add(listener); + } + } + + public void removeImageChangeListener(IImageChangeListener listener) { + synchronized (mImageChangeListeners) { + mImageChangeListeners.remove(listener); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java new file mode 100644 index 00000000..4e0972ff --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import com.android.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class manages the resource data that was dumped from a View's Theme. + */ +public class ThemeModel { + + private List data; + + public ThemeModel() { + data = new ArrayList(); + } + + public void add(@NonNull String name, @NonNull String value) { + data.add(new ThemeModelData(name, value)); + } + + public List getData() { + return data; + } + + public class ThemeModelData { + private String mName; + private String mValue; + + public ThemeModelData(@NonNull String name, @NonNull String value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public String getValue() { + return mValue; + } + } +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java new file mode 100644 index 00000000..0eb96c95 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import java.util.ArrayList; + +public class TreeViewModel { + public static final double MAX_ZOOM = 2; + + public static final double MIN_ZOOM = 0.2; + + private Window mWindow; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Rectangle mViewport; + + private double mZoom; + + private final ArrayList mTreeChangeListeners = + new ArrayList(); + + private static TreeViewModel sModel; + + public static TreeViewModel getModel() { + if (sModel == null) { + sModel = new TreeViewModel(); + } + return sModel; + } + + public void setData(Window window, ViewNode viewNode) { + synchronized (this) { + if (mTree != null) { + mTree.viewNode.dispose(); + } + this.mWindow = window; + if (viewNode == null) { + mTree = null; + } else { + mTree = new DrawableViewNode(viewNode); + mTree.setLeft(); + mTree.placeRoot(); + } + mViewport = null; + mZoom = 1; + mSelectedNode = null; + } + notifyTreeChanged(); + } + + public void setSelection(DrawableViewNode selectedNode) { + synchronized (this) { + this.mSelectedNode = selectedNode; + } + notifySelectionChanged(); + } + + public void setViewport(Rectangle viewport) { + synchronized (this) { + this.mViewport = viewport; + } + notifyViewportChanged(); + } + + public void setZoom(double newZoom) { + Point zoomPoint = null; + synchronized (this) { + if (mTree != null && mViewport != null) { + zoomPoint = + new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height / 2); + } + } + zoomOnPoint(newZoom, zoomPoint); + } + + public void zoomOnPoint(double newZoom, Point zoomPoint) { + synchronized (this) { + if (mTree != null && this.mViewport != null) { + if (newZoom < MIN_ZOOM) { + newZoom = MIN_ZOOM; + } + if (newZoom > MAX_ZOOM) { + newZoom = MAX_ZOOM; + } + mViewport.x = zoomPoint.x - (zoomPoint.x - mViewport.x) * mZoom / newZoom; + mViewport.y = zoomPoint.y - (zoomPoint.y - mViewport.y) * mZoom / newZoom; + mViewport.width = mViewport.width * mZoom / newZoom; + mViewport.height = mViewport.height * mZoom / newZoom; + mZoom = newZoom; + } + } + notifyZoomChanged(); + } + + public DrawableViewNode getTree() { + synchronized (this) { + return mTree; + } + } + + public Window getWindow() { + synchronized (this) { + return mWindow; + } + } + + public Rectangle getViewport() { + synchronized (this) { + return mViewport; + } + } + + public double getZoom() { + synchronized (this) { + return mZoom; + } + } + + public DrawableViewNode getSelection() { + synchronized (this) { + return mSelectedNode; + } + } + + public static interface ITreeChangeListener { + public void treeChanged(); + + public void selectionChanged(); + + public void viewportChanged(); + + public void zoomChanged(); + } + + private ITreeChangeListener[] getTreeChangeListenerList() { + ITreeChangeListener[] listeners = null; + synchronized (mTreeChangeListeners) { + if (mTreeChangeListeners.size() == 0) { + return null; + } + listeners = + mTreeChangeListeners.toArray(new ITreeChangeListener[mTreeChangeListeners.size()]); + } + return listeners; + } + + public void notifyTreeChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].treeChanged(); + } + } + } + + public void notifySelectionChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].selectionChanged(); + } + } + } + + public void notifyViewportChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].viewportChanged(); + } + } + } + + public void notifyZoomChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].zoomChanged(); + } + } + } + + public void addTreeChangeListener(ITreeChangeListener listener) { + synchronized (mTreeChangeListeners) { + mTreeChangeListeners.add(listener); + } + } + + public void removeTreeChangeListener(ITreeChangeListener listener) { + synchronized (mTreeChangeListeners) { + mTreeChangeListeners.remove(listener); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java new file mode 100644 index 00000000..cb00888a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import org.eclipse.swt.graphics.Image; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public class ViewNode { + + public static enum ProfileRating { + RED, YELLOW, GREEN, NONE + }; + + private static final double RED_THRESHOLD = 0.8; + + private static final double YELLOW_THRESHOLD = 0.5; + + public static final String MISCELLANIOUS = "miscellaneous"; + + public String id; + + public String name; + + public String hashCode; + + public List properties = new ArrayList(); + + public Map namedProperties = new HashMap(); + + public ViewNode parent; + + public List children = new ArrayList(); + + public int left; + + public int top; + + public int width; + + public int height; + + public int scrollX; + + public int scrollY; + + public int paddingLeft; + + public int paddingRight; + + public int paddingTop; + + public int paddingBottom; + + public int marginLeft; + + public int marginRight; + + public int marginTop; + + public int marginBottom; + + public int baseline; + + public boolean willNotDraw; + + public boolean hasMargins; + + public boolean hasFocus; + + public int index; + + public double measureTime; + + public double layoutTime; + + public double drawTime; + + public ProfileRating measureRating = ProfileRating.NONE; + + public ProfileRating layoutRating = ProfileRating.NONE; + + public ProfileRating drawRating = ProfileRating.NONE; + + public Set categories = new TreeSet(); + + public Window window; + + public Image image; + + public int imageReferences = 1; + + public int viewCount; + + public boolean filtered; + + public int protocolVersion; + + public ViewNode(Window window, ViewNode parent, String data) { + this.window = window; + this.parent = parent; + index = this.parent == null ? 0 : this.parent.children.size(); + if (this.parent != null) { + this.parent.children.add(this); + } + int delimIndex = data.indexOf('@'); + if (delimIndex < 0) { + throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data); + } + name = data.substring(0, delimIndex); + data = data.substring(delimIndex + 1); + delimIndex = data.indexOf(' '); + hashCode = data.substring(0, delimIndex); + + if (data.length() > delimIndex + 1) { + loadProperties(data.substring(delimIndex + 1).trim()); + } else { + // defaults in case properties are not available + id = "unknown"; + width = height = 10; + } + + measureTime = -1; + layoutTime = -1; + drawTime = -1; + } + + public void dispose() { + final int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).dispose(); + } + dereferenceImage(); + } + + public void referenceImage() { + imageReferences++; + } + + public void dereferenceImage() { + imageReferences--; + if (image != null && imageReferences == 0) { + image.dispose(); + } + } + + private void loadProperties(String data) { + int start = 0; + boolean stop; + do { + int index = data.indexOf('=', start); + ViewNode.Property property = new ViewNode.Property(); + property.name = data.substring(start, index); + + int index2 = data.indexOf(',', index + 1); + int length = Integer.parseInt(data.substring(index + 1, index2)); + start = index2 + 1 + length; + property.value = data.substring(index2 + 1, index2 + 1 + length); + + properties.add(property); + namedProperties.put(property.name, property); + + stop = start >= data.length(); + if (!stop) { + start += 1; + } + } while (!stop); + + Collections.sort(properties, new Comparator() { + @Override + public int compare(ViewNode.Property source, ViewNode.Property destination) { + return source.name.compareTo(destination.name); + } + }); + + id = namedProperties.get("mID").value; //$NON-NLS-1$ + + left = + namedProperties.containsKey("mLeft") ? getInt("mLeft", 0) : getInt("layout:mLeft", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + 0); + top = namedProperties.containsKey("mTop") ? getInt("mTop", 0) : getInt("layout:mTop", 0); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + width = + namedProperties.containsKey("getWidth()") ? getInt("getWidth()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getWidth()", 0); //$NON-NLS-1$ + height = + namedProperties.containsKey("getHeight()") ? getInt("getHeight()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getHeight()", 0); //$NON-NLS-1$ + scrollX = + namedProperties.containsKey("mScrollX") ? getInt("mScrollX", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "scrolling:mScrollX", 0); //$NON-NLS-1$ + scrollY = + namedProperties.containsKey("mScrollY") ? getInt("mScrollY", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "scrolling:mScrollY", 0); //$NON-NLS-1$ + paddingLeft = + namedProperties.containsKey("mPaddingLeft") ? getInt("mPaddingLeft", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingLeft", 0); //$NON-NLS-1$ + paddingRight = + namedProperties.containsKey("mPaddingRight") ? getInt("mPaddingRight", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingRight", 0); //$NON-NLS-1$ + paddingTop = + namedProperties.containsKey("mPaddingTop") ? getInt("mPaddingTop", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingTop", 0); //$NON-NLS-1$ + paddingBottom = + namedProperties.containsKey("mPaddingBottom") ? getInt("mPaddingBottom", 0) //$NON-NLS-1$ //$NON-NLS-2$ + : getInt("padding:mPaddingBottom", 0); //$NON-NLS-1$ + marginLeft = + namedProperties.containsKey("layout_leftMargin") ? getInt("layout_leftMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_leftMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginRight = + namedProperties.containsKey("layout_rightMargin") ? getInt("layout_rightMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_rightMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginTop = + namedProperties.containsKey("layout_topMargin") ? getInt("layout_topMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_topMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginBottom = + namedProperties.containsKey("layout_bottomMargin") ? getInt("layout_bottomMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) + : getInt("layout:layout_bottomMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + baseline = + namedProperties.containsKey("getBaseline()") ? getInt("getBaseline()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getBaseline()", 0); //$NON-NLS-1$ + willNotDraw = + namedProperties.containsKey("willNotDraw()") ? getBoolean("willNotDraw()", false) //$NON-NLS-1$ //$NON-NLS-2$ + : getBoolean("drawing:willNotDraw()", false); //$NON-NLS-1$ + hasFocus = + namedProperties.containsKey("hasFocus()") ? getBoolean("hasFocus()", false) //$NON-NLS-1$ //$NON-NLS-2$ + : getBoolean("focus:hasFocus()", false); //$NON-NLS-1$ + + hasMargins = + marginLeft != Integer.MIN_VALUE && marginRight != Integer.MIN_VALUE + && marginTop != Integer.MIN_VALUE && marginBottom != Integer.MIN_VALUE; + + for (String name : namedProperties.keySet()) { + int index = name.indexOf(':'); + if (index != -1) { + categories.add(name.substring(0, index)); + } + } + if (categories.size() != 0) { + categories.add(MISCELLANIOUS); + } + } + + public void setProfileRatings() { + final int N = children.size(); + if (N > 1) { + double totalMeasure = 0; + double totalLayout = 0; + double totalDraw = 0; + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + totalMeasure += child.measureTime; + totalLayout += child.layoutTime; + totalDraw += child.drawTime; + } + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + if (child.measureTime / totalMeasure >= RED_THRESHOLD) { + child.measureRating = ProfileRating.RED; + } else if (child.measureTime / totalMeasure >= YELLOW_THRESHOLD) { + child.measureRating = ProfileRating.YELLOW; + } else { + child.measureRating = ProfileRating.GREEN; + } + if (child.layoutTime / totalLayout >= RED_THRESHOLD) { + child.layoutRating = ProfileRating.RED; + } else if (child.layoutTime / totalLayout >= YELLOW_THRESHOLD) { + child.layoutRating = ProfileRating.YELLOW; + } else { + child.layoutRating = ProfileRating.GREEN; + } + if (child.drawTime / totalDraw >= RED_THRESHOLD) { + child.drawRating = ProfileRating.RED; + } else if (child.drawTime / totalDraw >= YELLOW_THRESHOLD) { + child.drawRating = ProfileRating.YELLOW; + } else { + child.drawRating = ProfileRating.GREEN; + } + } + } + for (int i = 0; i < N; i++) { + children.get(i).setProfileRatings(); + } + } + + public void setViewCount() { + viewCount = 1; + final int N = children.size(); + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + child.setViewCount(); + viewCount += child.viewCount; + } + } + + public void filter(String text) { + int dotIndex = name.lastIndexOf('.'); + String shortName = (dotIndex == -1) ? name : name.substring(dotIndex + 1); + filtered = + !text.equals("") //$NON-NLS-1$ + && (shortName.toLowerCase().contains(text.toLowerCase()) || (!id + .equals("NO_ID") && id.toLowerCase().contains(text.toLowerCase()))); //$NON-NLS-1$ + final int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).filter(text); + } + } + + private boolean getBoolean(String name, boolean defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Boolean.parseBoolean(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + private int getInt(String name, int defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Integer.parseInt(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + @Override + public String toString() { + return name + "@" + hashCode; //$NON-NLS-1$ + } + + public static class Property { + public String name; + + public String value; + + @Override + public String toString() { + return name + '=' + value; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java new file mode 100644 index 00000000..a0a12917 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewerlib.device.IHvDevice; + +/** + * Used for storing a window from the window manager service on the device. + * These are the windows that the device selector shows. + */ +public class Window { + private final String mTitle; + private final int mHashCode; + private final IHvDevice mHvDevice; + private final Client mClient; + + public Window(IHvDevice device, String title, int hashCode) { + mHvDevice = device; + mTitle = title; + mHashCode = hashCode; + mClient = null; + } + + public Window(IHvDevice device, String title, Client c) { + mHvDevice = device; + mTitle = title; + mClient = c; + mHashCode = c.hashCode(); + } + + public String getTitle() { + return mTitle; + } + + public int getHashCode() { + return mHashCode; + } + + public String encode() { + return Integer.toHexString(mHashCode); + } + + @Override + public String toString() { + return mTitle; + } + + public IHvDevice getHvDevice() { + return mHvDevice; + } + + public IDevice getDevice() { + return mHvDevice.getDevice(); + } + + public Client getClient() { + return mClient; + } + + public static Window getFocusedWindow(IHvDevice device) { + return new Window(device, "", -1); + } + + /* + * After each refresh of the windows in the device selector, the windows are + * different instances and automatically reselecting the same window doesn't + * work in the device selector unless the equals method is defined here. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + Window other = (Window) obj; + if (mHvDevice == null) { + if (other.mHvDevice != null) + return false; + } else if (!mHvDevice.getDevice().getSerialNumber().equals( + other.mHvDevice.getDevice().getSerialNumber())) + return false; + + if (mHashCode != other.mHashCode) + return false; + + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((mHvDevice == null) ? 0 : mHvDevice.getDevice().getSerialNumber().hashCode()); + result = prime * result + mHashCode; + return result; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java new file mode 100644 index 00000000..67911dcc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.ViewNode; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class CaptureDisplay { + private static Shell sShell; + + private static Canvas sCanvas; + + private static Image sImage; + + private static ViewNode sViewNode; + + private static Composite sButtonBar; + + private static Button sOnWhite; + + private static Button sOnBlack; + + private static Button sShowExtras; + + public static void show(Shell parentShell, ViewNode viewNode, Image image) { + if (sShell == null) { + createShell(); + } + if (sShell.isVisible() && CaptureDisplay.sViewNode != null) { + CaptureDisplay.sViewNode.dereferenceImage(); + } + CaptureDisplay.sImage = image; + CaptureDisplay.sViewNode = viewNode; + viewNode.referenceImage(); + sShell.setText(viewNode.name); + + boolean shellVisible = sShell.isVisible(); + if (!shellVisible) { + sShell.setSize(0, 0); + } + Rectangle bounds = + sShell.computeTrim(0, 0, Math.max(sButtonBar.getBounds().width, + image.getBounds().width), sButtonBar.getBounds().height + + image.getBounds().height + 5); + sShell.setSize(bounds.width, bounds.height); + if (!shellVisible) { + sShell.setLocation(parentShell.getBounds().x + + (parentShell.getBounds().width - bounds.width) / 2, parentShell.getBounds().y + + (parentShell.getBounds().height - bounds.height) / 2); + } + sShell.open(); + if (shellVisible) { + sCanvas.redraw(); + } + } + + private static void createShell() { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + GridLayout gridLayout = new GridLayout(); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + sShell.setLayout(gridLayout); + + sButtonBar = new Composite(sShell, SWT.NONE); + RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); + rowLayout.pack = true; + rowLayout.center = true; + sButtonBar.setLayout(rowLayout); + Composite buttons = new Composite(sButtonBar, SWT.NONE); + buttons.setLayout(new FillLayout()); + + sOnWhite = new Button(buttons, SWT.TOGGLE); + sOnWhite.setText("On White"); + sOnBlack = new Button(buttons, SWT.TOGGLE); + sOnBlack.setText("On Black"); + sOnBlack.setSelection(true); + sOnWhite.addSelectionListener(sWhiteSelectionListener); + sOnBlack.addSelectionListener(sBlackSelectionListener); + + sShowExtras = new Button(sButtonBar, SWT.CHECK); + sShowExtras.setText("Show Extras"); + sShowExtras.addSelectionListener(sExtrasSelectionListener); + + sCanvas = new Canvas(sShell, SWT.NONE); + sCanvas.setLayoutData(new GridData(GridData.FILL_BOTH)); + sCanvas.addPaintListener(sPaintListener); + + sShell.addShellListener(sShellListener); + + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + Image image = imageFactory.getImageByName("display.png"); //$NON-NLS-1$ + sShell.setImage(image); + } + + private static PaintListener sPaintListener = new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + if (sOnWhite.getSelection()) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } else { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } + e.gc.fillRectangle(0, 0, sCanvas.getBounds().width, sCanvas.getBounds().height); + if (sImage != null) { + int width = sImage.getBounds().width; + int height = sImage.getBounds().height; + int x = (sCanvas.getBounds().width - width) / 2; + int y = (sCanvas.getBounds().height - height) / 2; + e.gc.drawImage(sImage, x, y); + if (sShowExtras.getSelection()) { + if ((sViewNode.paddingLeft | sViewNode.paddingRight | sViewNode.paddingTop | sViewNode.paddingBottom) != 0) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE)); + e.gc.drawRectangle(x + sViewNode.paddingLeft, y + sViewNode.paddingTop, width + - sViewNode.paddingLeft - sViewNode.paddingRight - 1, height + - sViewNode.paddingTop - sViewNode.paddingBottom - 1); + } + if (sViewNode.hasMargins) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN)); + e.gc.drawRectangle(x - sViewNode.marginLeft, y - sViewNode.marginTop, width + + sViewNode.marginLeft + sViewNode.marginRight - 1, height + + sViewNode.marginTop + sViewNode.marginBottom - 1); + } + if (sViewNode.baseline != -1) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); + e.gc.drawLine(x, y + sViewNode.baseline, x + width - 1, sViewNode.baseline); + } + } + } + } + }; + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + if (sViewNode != null) { + sViewNode.dereferenceImage(); + } + } + + }; + + private static SelectionListener sWhiteSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sOnWhite.setSelection(true); + sOnBlack.setSelection(false); + sCanvas.redraw(); + } + }; + + private static SelectionListener sBlackSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sOnBlack.setSelection(true); + sOnWhite.setSelection(false); + sCanvas.redraw(); + } + }; + + private static SelectionListener sExtrasSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sCanvas.redraw(); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java new file mode 100644 index 00000000..991c0373 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.SdkConstants; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.ViewNode.Property; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.text.NumberFormat; + +public class DevicePropertyEditingSupport { + public enum PropertyType { + INTEGER, + INTEGER_OR_CONSTANT, + ENUM, + }; + + private static final List sDevicePropertyEditors = Arrays.asList( + new LayoutPropertyEditor(), + new PaddingPropertyEditor() + ); + + public boolean canEdit(Property p) { + return getPropertyEditorFor(p) != null; + } + + private IDevicePropertyEditor getPropertyEditorFor(Property p) { + for (IDevicePropertyEditor pe: sDevicePropertyEditors) { + if (pe.canEdit(p)) { + return pe; + } + } + + return null; + } + + public PropertyType getPropertyType(Property p) { + return getPropertyEditorFor(p).getType(p); + } + + public String[] getPropertyRange(Property p) { + return getPropertyEditorFor(p).getPropertyRange(p); + } + + public boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device) { + return getPropertyEditorFor(p).setValue(properties, p, newValue, viewNode, device); + } + + private static String stripCategoryPrefix(String name) { + return name.substring(name.indexOf(':') + 1); + } + + private interface IDevicePropertyEditor { + boolean canEdit(Property p); + PropertyType getType(Property p); + String[] getPropertyRange(Property p); + boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device); + } + + private static class LayoutPropertyEditor implements IDevicePropertyEditor { + private static final Set sLayoutPropertiesWithStringValues = + ImmutableSet.of(SdkConstants.ATTR_LAYOUT_WIDTH, + SdkConstants.ATTR_LAYOUT_HEIGHT, + SdkConstants.ATTR_LAYOUT_GRAVITY); + + private static final int MATCH_PARENT = -1; + private static final int FILL_PARENT = -1; + private static final int WRAP_CONTENT = -2; + + private enum LayoutGravity { + top(0x30), + bottom(0x50), + left(0x03), + right(0x05), + center_vertical(0x10), + fill_vertical(0x70), + center_horizontal(0x01), + fill_horizontal(0x07), + center(0x11), + fill(0x77), + clip_vertical(0x80), + clip_horizontal(0x08), + start(0x00800003), + end(0x00800005); + + private final int mValue; + + private LayoutGravity(int v) { + mValue = v; + } + } + + /** + * Returns true if this is a layout property with either a known string value, or an + * integer value. + */ + @Override + public boolean canEdit(Property p) { + String name = stripCategoryPrefix(p.name); + if (!name.startsWith(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX)) { + return false; + } + + if (sLayoutPropertiesWithStringValues.contains(name)) { + return true; + } + + try { + parseLocalizedInt(p.value); + return true; + } catch (ParseException e) { + return false; + } + } + + @Override + public PropertyType getType(Property p) { + String name = stripCategoryPrefix(p.name); + if (sLayoutPropertiesWithStringValues.contains(name)) { + return PropertyType.INTEGER_OR_CONSTANT; + } else { + return PropertyType.INTEGER; + } + } + + @Override + public String[] getPropertyRange(Property p) { + return new String[0]; + } + + @Override + public boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device) { + String name = stripCategoryPrefix(p.name); + + // nothing to do if same as current value + if (p.value.equals(newValue)) { + return false; + } + + int value = -1; + String textValue = null; + + if (SdkConstants.ATTR_LAYOUT_GRAVITY.equals(name)) { + value = 0; + StringBuilder sb = new StringBuilder(20); + for (String attr: Splitter.on('|').split((String) newValue)) { + LayoutGravity g; + try { + g = LayoutGravity.valueOf(attr); + } catch (IllegalArgumentException e) { + // ignore this gravity attribute + continue; + } + + value |= g.mValue; + + if (sb.length() > 0) { + sb.append('|'); + } + sb.append(g.name()); + } + textValue = sb.toString(); + } else if (SdkConstants.ATTR_LAYOUT_HEIGHT.equals(name) + || SdkConstants.ATTR_LAYOUT_WIDTH.equals(name)) { + // newValue is of type string, but its contents may be a named constant or a integer + String s = (String) newValue; + if (s.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) { + textValue = SdkConstants.VALUE_MATCH_PARENT; + value = MATCH_PARENT; + } else if (s.equalsIgnoreCase(SdkConstants.VALUE_FILL_PARENT)) { + textValue = SdkConstants.VALUE_FILL_PARENT; + value = FILL_PARENT; + } else if (s.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) { + textValue = SdkConstants.VALUE_WRAP_CONTENT; + value = WRAP_CONTENT; + } + } + + if (textValue == null) { + try { + value = Integer.parseInt((String) newValue); + } catch (NumberFormatException e) { + return false; + } + } + + // attempt to set the value on the device + name = name.substring(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX.length()); + if (device.setLayoutParameter(viewNode.window, viewNode, name, value)) { + p.value = textValue != null ? textValue : (String) newValue; + } + + return true; + } + } + + private static class PaddingPropertyEditor implements IDevicePropertyEditor { + // These names should match the field names used for padding in the Framework's View class + private static final String PADDING_LEFT = "mPaddingLeft"; //$NON-NLS-1$ + private static final String PADDING_RIGHT = "mPaddingRight"; //$NON-NLS-1$ + private static final String PADDING_TOP = "mPaddingTop"; //$NON-NLS-1$ + private static final String PADDING_BOTTOM = "mPaddingBottom"; //$NON-NLS-1$ + + private static final Set sPaddingProperties = ImmutableSet.of( + PADDING_LEFT, PADDING_RIGHT, PADDING_TOP, PADDING_BOTTOM); + + @Override + public boolean canEdit(Property p) { + return sPaddingProperties.contains(stripCategoryPrefix(p.name)); + } + + @Override + public PropertyType getType(Property p) { + return PropertyType.INTEGER; + } + + @Override + public String[] getPropertyRange(Property p) { + return new String[0]; + } + + /** + * Set padding: Since the only view method is setPadding(l, t, r, b), we need access + * to all 4 padding's to update any particular one. + */ + @Override + public boolean setValue(Collection properties, Property prop, Object newValue, + ViewNode viewNode, IHvDevice device) { + int v; + try { + v = Integer.parseInt((String) newValue); + } catch (NumberFormatException e) { + return false; + } + + int pLeft = 0; + int pRight = 0; + int pTop = 0; + int pBottom = 0; + + String propName = stripCategoryPrefix(prop.name); + for (Property p: properties) { + String name = stripCategoryPrefix(p.name); + if (!sPaddingProperties.contains(name)) { + continue; + } + + if (name.equals(PADDING_LEFT)) { + pLeft = propName.equals(PADDING_LEFT) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_RIGHT)) { + pRight = propName.equals(PADDING_RIGHT) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_TOP)) { + pTop = propName.equals(PADDING_TOP) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_BOTTOM)) { + pBottom = propName.equals(PADDING_BOTTOM) ? + v : parseLocalizedInt(p.value, 0); + } + } + + // invoke setPadding() on the device + device.invokeViewMethod(viewNode.window, viewNode, "setPadding", Arrays.asList( + Integer.valueOf(pLeft), + Integer.valueOf(pTop), + Integer.valueOf(pRight), + Integer.valueOf(pBottom) + )); + + // update the value set in the property (to avoid reading all properties back from + // the device) + prop.value = Integer.toString(v); + return true; + } + } + + public static int parseLocalizedInt(String string) + throws ParseException + { + if (string.isEmpty()) { + return 0; + } + return NumberFormat.getIntegerInstance().parse(string).intValue(); + } + + public static int parseLocalizedInt(String string, int defaultValue) + { + try + { + return parseLocalizedInt(string); + } + catch (ParseException e) {} + return defaultValue; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java new file mode 100644 index 00000000..116f5d4a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.viewers.IFontProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +public class DeviceSelector extends Composite implements IWindowChangeListener, SelectionListener { + private TreeViewer mTreeViewer; + + private Tree mTree; + + private DeviceSelectionModel mModel; + + private Font mBoldFont; + + private Image mDeviceImage; + + private Image mEmulatorImage; + + // private final static int ICON_WIDTH = 16; + + private boolean mDoTreeViewStuff; + + private boolean mDoPixelPerfectStuff; + + private class ContentProvider implements ITreeContentProvider, ILabelProvider, IFontProvider { + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof IHvDevice && mDoTreeViewStuff) { + Window[] list = mModel.getWindows((IHvDevice) parentElement); + if (list != null) { + return list; + } + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof Window) { + return ((Window) element).getDevice(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof IHvDevice && mDoTreeViewStuff) { + Window[] list = mModel.getWindows((IHvDevice) element); + if (list != null) { + return list.length != 0; + } + } + return false; + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof DeviceSelectionModel) { + return mModel.getDevices(); + } + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getImage(Object element) { + if (element instanceof IHvDevice) { + if (((IHvDevice) element).getDevice().isEmulator()) { + return mEmulatorImage; + } + return mDeviceImage; + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof IHvDevice) { + return ((IHvDevice) element).getDevice().getName(); + } else if (element instanceof Window) { + return ((Window) element).getTitle(); + } + return null; + } + + @Override + public Font getFont(Object element) { + if (element instanceof Window) { + int focusedWindow = mModel.getFocusedWindow(((Window) element).getHvDevice()); + if (focusedWindow == ((Window) element).getHashCode()) { + return mBoldFont; + } + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public DeviceSelector(Composite parent, boolean doTreeViewStuff, boolean doPixelPerfectStuff) { + super(parent, SWT.NONE); + this.mDoTreeViewStuff = doTreeViewStuff; + this.mDoPixelPerfectStuff = doPixelPerfectStuff; + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.SINGLE); + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + mTree = mTreeViewer.getTree(); + mTree.setLinesVisible(true); + mTree.addSelectionListener(this); + + addDisposeListener(mDisposeListener); + + loadResources(); + + mModel = DeviceSelectionModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mModel.addWindowChangeListener(this); + mTreeViewer.setInput(mModel); + + addControlListener(mControlListener); + } + + public void loadResources() { + Display display = Display.getDefault(); + Font systemFont = display.getSystemFont(); + FontData[] fontData = systemFont.getFontData(); + FontData[] newFontData = new FontData[fontData.length]; + for (int i = 0; i < fontData.length; i++) { + newFontData[i] = + new FontData(fontData[i].getName(), fontData[i].getHeight(), fontData[i] + .getStyle() + | SWT.BOLD); + } + mBoldFont = new Font(Display.getDefault(), newFontData); + + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mDeviceImage = + imageFactory.getImageByName("device.png"); + + mEmulatorImage = + imageFactory.getImageByName("emulator.png"); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeWindowChangeListener(DeviceSelector.this); + mBoldFont.dispose(); + } + }; + + // If the window gets too small, hide the data, otherwise SWT throws an + // ERROR. + + private ControlListener mControlListener = new ControlAdapter() { + private boolean noInput = false; + + @Override + public void controlResized(ControlEvent e) { + if (getBounds().height <= 38) { + mTreeViewer.setInput(null); + noInput = true; + } else if (noInput) { + mTreeViewer.setInput(mModel); + noInput = false; + } + } + }; + + @Override + public boolean setFocus() { + return mTree.setFocus(); + } + + public void setMode(boolean doTreeViewStuff, boolean doPixelPerfectStuff) { + if (this.mDoTreeViewStuff != doTreeViewStuff + || this.mDoPixelPerfectStuff != doPixelPerfectStuff) { + final boolean expandAll = !this.mDoTreeViewStuff && doTreeViewStuff; + this.mDoTreeViewStuff = doTreeViewStuff; + this.mDoPixelPerfectStuff = doPixelPerfectStuff; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + if (expandAll) { + mTreeViewer.expandAll(); + } + } + }); + } + } + + @Override + public void deviceConnected(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + mTreeViewer.setExpandedState(device, true); + } + }); + } + + @Override + public void deviceChanged(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); + mTreeViewer.refresh(device); + if (selection.getFirstElement() instanceof Window + && ((Window) selection.getFirstElement()).getDevice() == device) { + mTreeViewer.setSelection(selection, true); + } + } + }); + } + + @Override + public void deviceDisconnected(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + } + }); + } + + @Override + public void focusChanged(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); + mTreeViewer.refresh(device); + if (selection.getFirstElement() instanceof Window + && ((Window) selection.getFirstElement()).getDevice() == device) { + mTreeViewer.setSelection(selection, true); + } + } + }); + } + + @Override + public void selectionChanged(IHvDevice device, Window window) { + // pass + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + Object selection = ((TreeItem) e.item).getData(); + if (selection instanceof IHvDevice && mDoPixelPerfectStuff) { + HierarchyViewerDirector.getDirector().loadPixelPerfectData((IHvDevice) selection); + } else if (selection instanceof Window && mDoTreeViewStuff) { + HierarchyViewerDirector.getDirector().loadViewTreeData((Window) selection); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + TreeItem item = (TreeItem) e.item; + if (item == null) return; + Object selection = item.getData(); + if (selection instanceof IHvDevice) { + mModel.setSelection((IHvDevice) selection, null); + } else if (selection instanceof Window) { + mModel.setSelection(((Window) selection).getHvDevice(), (Window) selection); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java new file mode 100644 index 00000000..d105c2b0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.annotations.NonNull; +import com.android.hierarchyviewerlib.models.ThemeModel; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +import java.util.List; + + +public class DumpThemeDisplay { + private static final int DEFAULT_HEIGHT = 600; // px + private static final int NUM_COLUMNS = 2; + + private static Shell sShell; + private static ThemeModel sModel; + private static Text sSearchText; + private static Table sTable; + + public static void show(Shell parentShell, ThemeModel model) { + if (sShell == null) { + buildContents(); + } else { + sSearchText.setText(""); + sTable.removeAll(); + } + + sModel = model; + addTableItems("", sModel.getData()); + + // configure size and placement + sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); + for (int i = 0; i < NUM_COLUMNS; ++i) { + sTable.getColumn(i).pack(); + } + sTable.setLayoutData(GridDataFactory.swtDefaults().hint( + sTable.computeSize(SWT.DEFAULT, SWT.DEFAULT).x, DEFAULT_HEIGHT).create()); + sShell.pack(); + sShell.open(); + } + + private static void addTableItem(String name, String value) { + TableItem row = new TableItem(sTable, SWT.NONE); + row.setText(0, name); + row.setText(1, value); + } + + private static String sanitize(@NonNull String text) { + return text.toLowerCase().trim(); + } + + private static void addTableItems(String searchText, + List list) { + for (ThemeModel.ThemeModelData data : list) { + searchText = sanitize(searchText); + + if ("".equals(searchText)) { + addTableItem(data.getName(), data.getValue()); + } else { + if (sanitize(data.getName()).contains(searchText) + || sanitize(data.getValue()).contains(searchText)) { + addTableItem(data.getName(), data.getValue()); + } + } + } + } + + private static void buildContents() { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + sShell.setText("Dump Theme"); + sShell.addShellListener(sShellListener); + sShell.setLayout(new GridLayout()); + + sSearchText = new Text(sShell, SWT.SINGLE | SWT.BORDER); + sSearchText.setMessage("Enter text to search list"); + sSearchText.addModifyListener(sModifyListener); + + sTable = new Table(sShell, SWT.BORDER | SWT.FULL_SELECTION); + sTable.setHeaderVisible(true); + sTable.setLinesVisible(true); + + String[] headers = { "Resource Name", "Resource Value" }; + for (int i = 0; i < headers.length; ++i) { + TableColumn column = new TableColumn(sTable, SWT.NONE); + column.setText(headers[i]); + } + } + + private static ModifyListener sModifyListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + String searchText = sanitize(sSearchText.getText()); + sTable.removeAll(); + addTableItems(searchText, sModel.getData()); + } + }; + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java new file mode 100644 index 00000000..0d1b191d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java @@ -0,0 +1,537 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; +import com.android.hierarchyviewerlib.models.ViewNode.Property; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.lang.Math; +import java.lang.Override; +import java.lang.StringBuilder; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +public class EvaluateContrastDisplay { + private static final int DEFAULT_HEIGHT = 600; // px + private static final int MARGIN = 30; // px + private static final int PALLETE_IMAGE_SIZE = 16; // px + private static final int IMAGE_WIDTH = 800; // px + private static final int RESULTS_PANEL_WIDTH = 300; // px + private static final int MAX_NUM_CHARACTERS = 35; + + private static final String ABBREVIATE_SUFFIX = "...\""; + + private static Shell sShell; + private static Canvas sCanvas; + private static Composite sResultsPanel; + private static Tree sResultsTree; + + private static Image sImage; + private static Point sImageOffset; + private static ScrollBar sImageScrollBar; + private static int sImageWidth; + private static int sImageHeight; + + private static Image sYellowImage; + private static Image sRedImage; + private static Image sGreenImage; + + private static ViewNode sSelectedNode; + + private static org.eclipse.swt.graphics.Color sBorderColorPass; + private static org.eclipse.swt.graphics.Color sBorderColorFail; + private static org.eclipse.swt.graphics.Color sBorderColorIndeterminate; + private static org.eclipse.swt.graphics.Color sBorderColorCurrentlySelected; + + private static HashMap sRectangleForViewNode; + private static HashMap sBorderColorForViewNode; + private static HashMap sViewNodeForModel; + private static HashMap sImageForColor; + private static HashMap sViewNodeForTreeItem; + + private static double sScaleFactor; + + static { + sImageForColor = new HashMap(); + sViewNodeForTreeItem = new HashMap(); + + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sYellowImage = imageFactory.getImageByName("yellow.png"); + sRedImage = imageFactory.getImageByName("red.png"); + sGreenImage = imageFactory.getImageByName("green.png"); + + sRectangleForViewNode = new HashMap(); + sBorderColorForViewNode = new HashMap(); + sViewNodeForModel = new HashMap(); + } + + private static org.eclipse.swt.graphics.Color getBorderColorPass() { + if (sBorderColorPass == null) { + sBorderColorPass = /** green */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 255, 0)); + } + return sBorderColorPass; + } + + private static org.eclipse.swt.graphics.Color getBorderColorFail() { + if (sBorderColorFail == null) { + sBorderColorFail = /** red */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 0, 0)); + } + return sBorderColorFail; + } + + private static org.eclipse.swt.graphics.Color getBorderColorIndeterminate() { + if (sBorderColorIndeterminate == null) { + sBorderColorIndeterminate = /** yellow */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 255, 0)); + } + return sBorderColorIndeterminate; + } + + private static org.eclipse.swt.graphics.Color getBorderColorCurrentlySelected() { + if (sBorderColorCurrentlySelected == null) { + sBorderColorCurrentlySelected = /** blue */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 0, 255)); + } + return sBorderColorCurrentlySelected; + } + + private static void clear(boolean shellIsNull) { + sRectangleForViewNode.clear(); + sBorderColorForViewNode.clear(); + sViewNodeForModel.clear(); + + if (!shellIsNull) { + sImage.dispose(); + for (Image image : sImageForColor.values()) { + image.dispose(); + } + + sImageForColor.clear(); + sViewNodeForTreeItem.clear(); + for (Control item : sShell.getChildren()) { + item.dispose(); + } + } + + if (sBorderColorPass != null) { + sBorderColorPass.dispose(); + sBorderColorPass = null; + } + if (sBorderColorFail != null) { + sBorderColorFail.dispose(); + sBorderColorFail = null; + } + if (sBorderColorIndeterminate != null) { + sBorderColorIndeterminate.dispose(); + sBorderColorIndeterminate = null; + } + if (sBorderColorCurrentlySelected != null) { + sBorderColorCurrentlySelected.dispose(); + sBorderColorCurrentlySelected = null; + } + } + + private static Image scaleImage(Image image, int width, int height) { + Image scaled = new Image(Display.getDefault(), width, height); + GC gc = new GC(scaled); + gc.setInterpolation(SWT.HIGH); + gc.setAntialias(SWT.ON); + gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, + width, height); + image.dispose(); + gc.dispose(); + return scaled; + } + + public static void show(Shell parentShell, ViewNode rootNode, Image image) { + clear(sShell == null); + + sScaleFactor = Math.min(IMAGE_WIDTH / (double) image.getBounds().width, 1.0); + sImage = scaleImage(image, IMAGE_WIDTH, + (int) Math.round(image.getBounds().height * sScaleFactor)); + sImageWidth = sImage.getBounds().width; + sImageHeight = sImage.getBounds().height; + + if (sShell == null) { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + sShell.setText("Evaluate Contrast"); + sShell.addShellListener(sShellListener); + sShell.setLayout(new GridLayout(2, false)); + } + buildContents(sShell); + processEvaluatableChildViews(rootNode); + + sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); + sShell.setSize(IMAGE_WIDTH + RESULTS_PANEL_WIDTH + MARGIN, DEFAULT_HEIGHT + (MARGIN * 2)); + sImageScrollBar.setMaximum(sImage.getBounds().height); + sImageScrollBar.setThumb(DEFAULT_HEIGHT); + sShell.open(); + sShell.layout(); + } + + private static void buildContents(Composite shell) { + buildResultsPanel(); + buildImagePanel(shell); + } + + private static void buildResultsPanel() { + sResultsPanel = new Composite(sShell, SWT.NONE); + sResultsPanel.setLayout(new FillLayout()); + GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, true); + sResultsPanel.setLayoutData(gridData); + + ScrolledComposite scrolledComposite = new ScrolledComposite(sResultsPanel, SWT.VERTICAL); + sResultsTree = new Tree(scrolledComposite, SWT.NONE); + sResultsTree.setLinesVisible(true); + scrolledComposite.setContent(sResultsTree); + sResultsTree.setSize(RESULTS_PANEL_WIDTH, DEFAULT_HEIGHT); + + sResultsTree.addListener(SWT.PaintItem, new Listener() { + @Override + public void handleEvent(Event event) { + TreeItem item = (TreeItem) event.item; + Image image = (Image) item.getData(); + if (image != null) { + int x = event.x + event.width; + int itemHeight = sResultsTree.getItemHeight(); + int imageHeight = image.getBounds().height; + int y = event.y + (itemHeight - imageHeight) / 2; + event.gc.drawImage(image, x, y); + } + } + }); + + Listener listener = new Listener() { + @Override + public void handleEvent(Event e) { + TreeItem treeItem = (TreeItem) e.item; + if (treeItem.getItemCount() == 0) { + do { + treeItem = treeItem.getParentItem(); + } while (treeItem.getParentItem() != null); + } + + ViewNode node = sViewNodeForTreeItem.get(treeItem); + if (sSelectedNode != node) { + sSelectedNode = sViewNodeForTreeItem.get(treeItem); + sCanvas.redraw(); + } + } + }; + sResultsTree.addListener(SWT.Selection, listener); + sResultsTree.addListener(SWT.DefaultSelection, listener); + } + + private static void buildImagePanel(Composite parent) { + sImageOffset = new Point(0, 0); + sCanvas = new Canvas(parent, SWT.V_SCROLL | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE); + sCanvas.addPaintListener(new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + GC gc = e.gc; + gc.drawImage(sImage, sImageOffset.x, sImageOffset.y); + + for (ViewNode viewNode : sRectangleForViewNode.keySet()) { + Rectangle rectangle = sRectangleForViewNode.get(viewNode); + if (sSelectedNode == viewNode) { + e.gc.setForeground(getBorderColorCurrentlySelected()); + } else { + e.gc.setForeground(sBorderColorForViewNode.get(viewNode)); + } + e.gc.drawRectangle(Math.max(0, sImageOffset.x + rectangle.x - 1), + sImageOffset.y + rectangle.y - 1, + rectangle.width - 1, + rectangle.height - 1); + } + + Rectangle rect = sImage.getBounds(); + Rectangle client = sCanvas.getClientArea(); + int marginWidth = client.width - rect.width; + if (marginWidth > 0) { + gc.fillRectangle(rect.width, 0, marginWidth, client.height); + } + int marginHeight = client.height - rect.height; + if (marginHeight > 0) { + gc.fillRectangle(0, rect.height, client.width, marginHeight); + } + } + }); + + sImageScrollBar = sCanvas.getVerticalBar(); + sImageScrollBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + int offset = sImageScrollBar.getSelection(); + Rectangle imageBounds = sImage.getBounds(); + sImageOffset.y = -offset; + + int y = -offset - sImageOffset.y; + sCanvas.scroll(0, y, 0, 0, imageBounds.width, imageBounds.height, false); + sCanvas.redraw(); + } + }); + + GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, true, true); + gridData.widthHint = IMAGE_WIDTH + MARGIN; + gridData.heightHint = DEFAULT_HEIGHT; + sCanvas.setLayoutData(gridData); + } + + private static void processEvaluatableChildViews(ViewNode root){ + List children = getEvaluatableChildViews(root); + + for (final ViewNode child : children) { + calculateRectangleForViewNode(child); + EvaluateContrastModel evaluateContrastModel = evaluateContrastForView(child); + if (evaluateContrastModel != null) { + calculateBorderColorForViewNode(child, evaluateContrastModel.getContrastResult()); + buildTreeItem(evaluateContrastModel, child); + sViewNodeForModel.put(child, evaluateContrastModel); + } else { + sRectangleForViewNode.remove(child); + } + } + } + + private static void buildTreeItem(EvaluateContrastModel model, final ViewNode child) { + int dotIndex = child.name.lastIndexOf('.'); + String shortName = (dotIndex == -1) ? child.name : child.name.substring(dotIndex + 1); + String text = shortName + ": \"" + child.namedProperties.get("text:mText").value + "\""; + + TreeItem item = new TreeItem(sResultsTree, SWT.NONE); + item.setText(transformText(text, MAX_NUM_CHARACTERS)); + item.setImage(getResultImage(model.getContrastResult())); + sViewNodeForTreeItem.put(item, child); + buildTreeItemsForModel(model, item); + } + + private static Image buildImageForColor(int color) { + Image image = sImageForColor.get(color); + + if (image == null) { + image = new Image(Display.getDefault(), PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); + GC gc = new GC(image); + + org.eclipse.swt.graphics.Color swtColor = awtColortoSwtColor(new java.awt.Color(color)); + gc.setBackground(swtColor); + swtColor.dispose(); + gc.fillRectangle(0, 0, PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); + + swtColor = awtColortoSwtColor(java.awt.Color.BLACK); + gc.setForeground(swtColor); + swtColor.dispose(); + gc.drawRectangle(0, 0, PALLETE_IMAGE_SIZE - 1, PALLETE_IMAGE_SIZE - 1); + gc.dispose(); + + sImageForColor.put(color, image); + } + + return image; + } + + public static org.eclipse.swt.graphics.Color awtColortoSwtColor(java.awt.Color color) { + return new org.eclipse.swt.graphics.Color(Display.getDefault(), + color.getRed(), color.getGreen(), color.getBlue()); + } + + private static void buildTreeItemsForModel(EvaluateContrastModel model, TreeItem parent) { + TreeItem item = new TreeItem(parent, SWT.NONE); + item.setText("Text color: " + model.getTextColorHex()); + item.setData(buildImageForColor(model.getTextColor())); + + item = new TreeItem(parent, SWT.NONE); + item.setText("Background color: " + model.getBackgroundColorHex()); + item.setData(buildImageForColor(model.getBackgroundColor())); + + new TreeItem(parent, SWT.NONE).setText("Text size: " + model.getTextSize()); + + new TreeItem(parent, SWT.NONE).setText("Contrast ratio: " + String.format( + EvaluateContrastModel.CONTRAST_RATIO_FORMAT, model.getContrastRatio())); + + if (!model.isIndeterminate()) { + new TreeItem(parent, SWT.NONE).setText("Test: " + model.getContrastResult().name()); + } else { + item = new TreeItem(parent, SWT.NONE); + item.setText("Normal Text Test: " + model.getContrastResultForNormalText().name()); + item.setImage(getResultImage(model.getContrastResultForNormalText())); + item = new TreeItem(parent, SWT.NONE); + item.setText("Large Text Test: " + model.getContrastResultForLargeText().name()); + item.setImage(getResultImage(model.getContrastResultForLargeText())); + } + } + + private static List getEvaluatableChildViews(ViewNode root) { + List children = new ArrayList(); + + children.add(root); + for (int i = 0; i < children.size(); ++i) { + ViewNode node = children.get(i); + List temp = node.children; + for (ViewNode child: temp) { + if (!children.contains(child)) { + children.add(child); + } + } + } + + List evalutableChildren = new ArrayList(); + for (final ViewNode child : children) { + if (child.namedProperties.get("text:mText") != null) { + evalutableChildren.add(child); + } + } + + return evalutableChildren; + } + + private static void calculateBorderColorForViewNode(ViewNode node, ContrastResult result) { + org.eclipse.swt.graphics.Color borderColor; + + switch (result) { + case PASS: + borderColor = getBorderColorPass(); + break; + case FAIL: + borderColor = getBorderColorFail(); + break; + case INDETERMINATE: + default: + borderColor = getBorderColorIndeterminate(); + } + + sBorderColorForViewNode.put(node, borderColor); + } + + private static Image getResultImage(ContrastResult result) { + switch (result) { + case PASS: + return sGreenImage; + case FAIL: + return sRedImage; + default: + return sYellowImage; + } + } + + private static String transformText(String text, int maxNumCharacters) { + if (text.length() == maxNumCharacters) { + return text; + } else if (text.length() < maxNumCharacters) { + char[] filler = new char[maxNumCharacters - text.length()]; + Arrays.fill(filler,' '); + return text + new String(filler); + } + + StringBuilder abbreviatedText = new StringBuilder(); + abbreviatedText.append(text.substring(0, maxNumCharacters - ABBREVIATE_SUFFIX.length())); + abbreviatedText.append(ABBREVIATE_SUFFIX); + return abbreviatedText.toString(); + } + + private static void calculateRectangleForViewNode(ViewNode viewNode) { + int leftShift = 0; + int topShift = 0; + int nodeLeft = (int) Math.round(viewNode.left * sScaleFactor); + int nodeTop = (int) Math.round(viewNode.top * sScaleFactor); + int nodeWidth = (int) Math.round(viewNode.width * sScaleFactor); + int nodeHeight = (int) Math.round(viewNode.height * sScaleFactor); + ViewNode current = viewNode; + + while (current.parent != null) { + leftShift += (int) Math.round( + sScaleFactor * (current.parent.left - current.parent.scrollX)); + topShift += (int) Math.round( + sScaleFactor * (current.parent.top - current.parent.scrollY)); + current = current.parent; + } + + sRectangleForViewNode.put(viewNode, new Rectangle(leftShift + nodeLeft, + topShift + nodeTop, nodeWidth, nodeHeight)); + } + + private static EvaluateContrastModel evaluateContrastForView(ViewNode node) { + Map namedProperties = node.namedProperties; + Property textColorProperty = namedProperties.get("text:mCurTextColor"); + Integer textColor = textColorProperty == null ? null : + Integer.valueOf(textColorProperty.value); + Property textSizeProperty = namedProperties.get("text:getScaledTextSize()"); + Double textSize = textSizeProperty == null ? null : Double.valueOf(textSizeProperty.value); + Rectangle rectangle = sRectangleForViewNode.get(node); + Property boldProperty = namedProperties.get("text:getTypefaceStyle()"); + boolean isBold = boldProperty != null && boldProperty.value.equals("BOLD"); + + // TODO: also remove views that are covered by other views + if (rectangle.x < 0 || rectangle.x > sImageWidth || + rectangle.y < 0 || rectangle.y > sImageHeight || + rectangle.width == 0 || rectangle.height == 0) { + // not viewable in screenshot, therefore can't parse background color + return null; + } + + int x = Math.max(0, rectangle.x); + int y = Math.max(0, rectangle.y); + int width = Math.min(sImageWidth, rectangle.x + rectangle.width); + int height = Math.min(sImageHeight, rectangle.y + rectangle.height); + + return new EvaluateContrastModel( + sImage, textColor, textSize, x, y, width, height, isBold); + } + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + clear(sShell == null); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java new file mode 100644 index 00000000..41c2de4d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class InvokeMethodPrompt extends Composite implements ITreeChangeListener { + private TreeViewModel mModel; + private DrawableViewNode mSelectedNode; + private Text mText; + private static final Splitter CMD_SPLITTER = Splitter.on(CharMatcher.anyOf(", ")) + .trimResults().omitEmptyStrings(); + + public InvokeMethodPrompt(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + + mText = new Text(this, SWT.BORDER); + mText.addKeyListener(new KeyListener() { + @Override + public void keyReleased(KeyEvent ke) { + } + + @Override + public void keyPressed(KeyEvent ke) { + onKeyPress(ke); + } + }); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + } + + private void onKeyPress(KeyEvent ke) { + if (ke.keyCode == SWT.CR) { + String cmd = mText.getText().trim(); + if (!cmd.isEmpty()) { + invokeViewMethod(cmd); + } + mText.setText(""); + } + } + + private void invokeViewMethod(String cmd) { + Iterator segmentIterator = CMD_SPLITTER.split(cmd).iterator(); + + String method = null; + if (segmentIterator.hasNext()) { + method = segmentIterator.next(); + } else { + return; + } + + List args = new ArrayList(10); + while (segmentIterator.hasNext()) { + String arg = segmentIterator.next(); + + // check for boolean + if (arg.equalsIgnoreCase("true")) { + args.add(Boolean.TRUE); + continue; + } else if (arg.equalsIgnoreCase("false")) { + args.add(Boolean.FALSE); + continue; + } + + // see if last character gives a clue regarding the argument type + char typeSpecifier = Character.toUpperCase(arg.charAt(arg.length() - 1)); + try { + switch (typeSpecifier) { + case 'L': + args.add(Long.valueOf(arg.substring(0, arg.length()))); + break; + case 'D': + args.add(Double.valueOf(arg.substring(0, arg.length()))); + break; + case 'F': + args.add(Float.valueOf(arg.substring(0, arg.length()))); + break; + case 'S': + args.add(Short.valueOf(arg.substring(0, arg.length()))); + break; + case 'B': + args.add(Byte.valueOf(arg.substring(0, arg.length()))); + break; + default: // default to integer + args.add(Integer.valueOf(arg)); + break; + } + } catch (NumberFormatException e) { + Log.e("hv", "Unable to parse method argument: " + arg); + return; + } + } + + HierarchyViewerDirector.getDirector().invokeMethodOnSelectedView(method, args); + } + + @Override + public void selectionChanged() { + mSelectedNode = mModel.getSelection(); + refresh(); + } + + private boolean isViewUpdateEnabled(ViewNode viewNode) { + IHvDevice device = viewNode.window.getHvDevice(); + return device != null && device.isViewUpdateEnabled(); + } + + private void refresh() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mText.setEnabled(mSelectedNode != null + && isViewUpdateEnabled(mSelectedNode.viewNode)); + } + }); + } + + @Override + public void treeChanged() { + selectionChanged(); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java new file mode 100644 index 00000000..d3278feb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +import java.util.ArrayList; + +public class LayoutViewer extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Transform mTransform; + + private Transform mInverse; + + private double mScale; + + private boolean mShowExtras = false; + + private boolean mOnBlack = true; + + public LayoutViewer(Composite parent) { + super(parent, SWT.NONE); + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + addDisposeListener(mDisposeListener); + addPaintListener(mPaintListener); + addListener(SWT.Resize, mResizeListener); + addMouseListener(mMouseListener); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + treeChanged(); + } + + public void setShowExtras(boolean show) { + mShowExtras = show; + doRedraw(); + } + + public void setOnBlack(boolean value) { + mOnBlack = value; + doRedraw(); + } + + public boolean getOnBlack() { + return mOnBlack; + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(LayoutViewer.this); + mTransform.dispose(); + mInverse.dispose(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + } + }; + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + synchronized (this) { + setTransform(); + } + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (mSelectedNode != null) { + HierarchyViewerDirector.getDirector() + .showCapture(getShell(), mSelectedNode.viewNode); + } + } + + @Override + public void mouseDown(MouseEvent e) { + boolean selectionChanged = false; + DrawableViewNode newSelection = null; + synchronized (LayoutViewer.this) { + if (mTree != null) { + float[] pt = { + e.x, e.y + }; + mInverse.transform(pt); + newSelection = + updateSelection(mTree, pt[0], pt[1], 0, 0, 0, 0, mTree.viewNode.width, + mTree.viewNode.height); + if (mSelectedNode != newSelection) { + selectionChanged = true; + } + } + } + if (selectionChanged) { + mModel.setSelection(newSelection); + } + } + + @Override + public void mouseUp(MouseEvent e) { + // pass + } + }; + + private DrawableViewNode updateSelection(DrawableViewNode node, float x, float y, int left, + int top, int clipX, int clipY, int clipWidth, int clipHeight) { + if (!node.treeDrawn) { + return null; + } + // Update the clip + int x1 = Math.max(left, clipX); + int x2 = Math.min(left + node.viewNode.width, clipX + clipWidth); + int y1 = Math.max(top, clipY); + int y2 = Math.min(top + node.viewNode.height, clipY + clipHeight); + clipX = x1; + clipY = y1; + clipWidth = x2 - x1; + clipHeight = y2 - y1; + if (x < clipX || x > clipX + clipWidth || y < clipY || y > clipY + clipHeight) { + return null; + } + final int N = node.children.size(); + for (int i = N - 1; i >= 0; i--) { + DrawableViewNode child = node.children.get(i); + DrawableViewNode ret = + updateSelection(child, x, y, + left + child.viewNode.left - node.viewNode.scrollX, top + + child.viewNode.top - node.viewNode.scrollY, clipX, clipY, + clipWidth, clipHeight); + if (ret != null) { + return ret; + } + } + return node; + } + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (LayoutViewer.this) { + if (mOnBlack) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } else { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mTree != null) { + e.gc.setLineWidth((int) Math.ceil(0.3 / mScale)); + e.gc.setTransform(mTransform); + if (mOnBlack) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } else { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } + Rectangle parentClipping = e.gc.getClipping(); + e.gc.setClipping(0, 0, mTree.viewNode.width + (int) Math.ceil(0.3 / mScale), + mTree.viewNode.height + (int) Math.ceil(0.3 / mScale)); + paintRecursive(e.gc, mTree, 0, 0, true); + + if (mSelectedNode != null) { + e.gc.setClipping(parentClipping); + + // w00t, let's be nice and display the whole path in + // light red and the selected node in dark red. + ArrayList rightLeftDistances = new ArrayList(); + int left = 0; + int top = 0; + DrawableViewNode currentNode = mSelectedNode; + while (currentNode != mTree) { + left += currentNode.viewNode.left; + top += currentNode.viewNode.top; + currentNode = currentNode.parent; + left -= currentNode.viewNode.scrollX; + top -= currentNode.viewNode.scrollY; + rightLeftDistances.add(new Point(left, top)); + } + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED)); + currentNode = mSelectedNode.parent; + final int N = rightLeftDistances.size(); + for (int i = 0; i < N; i++) { + e.gc.drawRectangle((int) (left - rightLeftDistances.get(i).x), + (int) (top - rightLeftDistances.get(i).y), + currentNode.viewNode.width, currentNode.viewNode.height); + currentNode = currentNode.parent; + } + + if (mShowExtras && mSelectedNode.viewNode.image != null) { + e.gc.drawImage(mSelectedNode.viewNode.image, left, top); + if (mOnBlack) { + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_WHITE)); + } else { + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_BLACK)); + } + paintRecursive(e.gc, mSelectedNode, left, top, true); + + } + + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); + e.gc.setLineWidth((int) Math.ceil(2 / mScale)); + e.gc.drawRectangle(left, top, mSelectedNode.viewNode.width, + mSelectedNode.viewNode.height); + } + } + } + } + }; + + private void paintRecursive(GC gc, DrawableViewNode node, int left, int top, boolean root) { + if (!node.treeDrawn) { + return; + } + // Don't shift the root + if (!root) { + left += node.viewNode.left; + top += node.viewNode.top; + } + Rectangle parentClipping = gc.getClipping(); + int x1 = Math.max(parentClipping.x, left); + int x2 = + Math.min(parentClipping.x + parentClipping.width, left + node.viewNode.width + + (int) Math.ceil(0.3 / mScale)); + int y1 = Math.max(parentClipping.y, top); + int y2 = + Math.min(parentClipping.y + parentClipping.height, top + node.viewNode.height + + (int) Math.ceil(0.3 / mScale)); + + // Clipping is weird... You set it to -5 and it comes out 17 or + // something. + if (x2 <= x1 || y2 <= y1) { + return; + } + gc.setClipping(x1, y1, x2 - x1, y2 - y1); + final int N = node.children.size(); + for (int i = 0; i < N; i++) { + paintRecursive(gc, node.children.get(i), left - node.viewNode.scrollX, top + - node.viewNode.scrollY, false); + } + gc.setClipping(parentClipping); + if (!node.viewNode.willNotDraw) { + gc.drawRectangle(left, top, node.viewNode.width, node.viewNode.height); + } + + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + private void setTransform() { + if (mTree != null) { + Rectangle bounds = getBounds(); + int leftRightPadding = bounds.width <= 30 ? 0 : 5; + int topBottomPadding = bounds.height <= 30 ? 0 : 5; + mScale = + Math.min(1.0 * (bounds.width - leftRightPadding * 2) / mTree.viewNode.width, 1.0 + * (bounds.height - topBottomPadding * 2) / mTree.viewNode.height); + int scaledWidth = (int) Math.ceil(mTree.viewNode.width * mScale); + int scaledHeight = (int) Math.ceil(mTree.viewNode.height * mScale); + + mTransform.identity(); + mInverse.identity(); + mTransform.translate((bounds.width - scaledWidth) / 2.0f, + (bounds.height - scaledHeight) / 2.0f); + mInverse.translate((bounds.width - scaledWidth) / 2.0f, + (bounds.height - scaledHeight) / 2.0f); + mTransform.scale((float) mScale, (float) mScale); + mInverse.scale((float) mScale, (float) mScale); + if (bounds.width != 0 && bounds.height != 0) { + mInverse.invert(); + } + } + } + + @Override + public void selectionChanged() { + synchronized (this) { + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.referenceImage(); + } + } + doRedraw(); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.referenceImage(); + } + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java new file mode 100644 index 00000000..3973fed4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfect extends ScrolledComposite implements IImageChangeListener { + private Canvas mCanvas; + + private PixelPerfectModel mModel; + + private Image mImage; + + private Color mCrosshairColor; + + private Color mMarginColor; + + private Color mBorderColor; + + private Color mPaddingColor; + + private int mWidth; + + private int mHeight; + + private Point mCrosshairLocation; + + private ViewNode mSelectedNode; + + private Image mOverlayImage; + + private double mOverlayTransparency; + + public PixelPerfect(Composite parent) { + super(parent, SWT.H_SCROLL | SWT.V_SCROLL); + mCanvas = new Canvas(this, SWT.NONE); + setContent(mCanvas); + setExpandHorizontal(true); + setExpandVertical(true); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + mCanvas.addPaintListener(mPaintListener); + mCanvas.addMouseListener(mMouseListener); + mCanvas.addMouseMoveListener(mMouseMoveListener); + mCanvas.addKeyListener(mKeyListener); + + addDisposeListener(mDisposeListener); + + mCrosshairColor = new Color(Display.getDefault(), new RGB(0, 255, 255)); + mBorderColor = new Color(Display.getDefault(), new RGB(255, 0, 0)); + mMarginColor = new Color(Display.getDefault(), new RGB(0, 255, 0)); + mPaddingColor = new Color(Display.getDefault(), new RGB(0, 0, 255)); + + imageLoaded(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfect.this); + mCrosshairColor.dispose(); + mBorderColor.dispose(); + mPaddingColor.dispose(); + } + }; + + @Override + public boolean setFocus() { + return mCanvas.setFocus(); + } + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + handleMouseEvent(e); + } + + @Override + public void mouseUp(MouseEvent e) { + handleMouseEvent(e); + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + if (e.stateMask != 0) { + handleMouseEvent(e); + } + } + }; + + private void handleMouseEvent(MouseEvent e) { + synchronized (PixelPerfect.this) { + if (mImage == null) { + return; + } + int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; + int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; + e.x -= leftOffset; + e.y -= topOffset; + e.x = Math.max(e.x, 0); + e.x = Math.min(e.x, mWidth - 1); + e.y = Math.max(e.y, 0); + e.y = Math.min(e.y, mHeight - 1); + } + mModel.setCrosshairLocation(e.x, e.y); + } + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean crosshairMoved = false; + synchronized (PixelPerfect.this) { + if (mImage != null) { + switch (e.keyCode) { + case SWT.ARROW_UP: + if (mCrosshairLocation.y != 0) { + mCrosshairLocation.y--; + crosshairMoved = true; + } + break; + case SWT.ARROW_DOWN: + if (mCrosshairLocation.y != mHeight - 1) { + mCrosshairLocation.y++; + crosshairMoved = true; + } + break; + case SWT.ARROW_LEFT: + if (mCrosshairLocation.x != 0) { + mCrosshairLocation.x--; + crosshairMoved = true; + } + break; + case SWT.ARROW_RIGHT: + if (mCrosshairLocation.x != mWidth - 1) { + mCrosshairLocation.x++; + crosshairMoved = true; + } + break; + } + } + } + if (crosshairMoved) { + mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); + } + } + + @Override + public void keyReleased(KeyEvent e) { + // pass + } + + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfect.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, mCanvas.getSize().x, mCanvas.getSize().y); + if (mImage != null) { + // Let's be cool and put it in the center... + int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; + int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; + e.gc.drawImage(mImage, leftOffset, topOffset); + if (mOverlayImage != null) { + e.gc.setAlpha((int) (mOverlayTransparency * 255)); + int overlayTopOffset = + mCanvas.getSize().y / 2 + mHeight / 2 + - mOverlayImage.getBounds().height; + e.gc.drawImage(mOverlayImage, leftOffset, overlayTopOffset); + e.gc.setAlpha(255); + } + + if (mSelectedNode != null) { + // If the screen is in landscape mode, the + // coordinates are backwards. + int leftShift = 0; + int topShift = 0; + int nodeLeft = mSelectedNode.left; + int nodeTop = mSelectedNode.top; + int nodeWidth = mSelectedNode.width; + int nodeHeight = mSelectedNode.height; + int nodeMarginLeft = mSelectedNode.marginLeft; + int nodeMarginTop = mSelectedNode.marginTop; + int nodeMarginRight = mSelectedNode.marginRight; + int nodeMarginBottom = mSelectedNode.marginBottom; + int nodePadLeft = mSelectedNode.paddingLeft; + int nodePadTop = mSelectedNode.paddingTop; + int nodePadRight = mSelectedNode.paddingRight; + int nodePadBottom = mSelectedNode.paddingBottom; + ViewNode cur = mSelectedNode; + while (cur.parent != null) { + leftShift += cur.parent.left - cur.parent.scrollX; + topShift += cur.parent.top - cur.parent.scrollY; + cur = cur.parent; + } + + // Everything is sideways. + if (cur.width > cur.height) { + e.gc.setForeground(mPaddingColor); + e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight + + nodePadBottom, + topOffset + leftShift + nodeLeft + nodePadLeft, nodeHeight + - nodePadBottom - nodePadTop, nodeWidth - nodePadRight + - nodePadLeft); + e.gc.setForeground(mMarginColor); + e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight + - nodeMarginBottom, topOffset + leftShift + nodeLeft + - nodeMarginLeft, + nodeHeight + nodeMarginBottom + nodeMarginTop, nodeWidth + + nodeMarginRight + nodeMarginLeft); + e.gc.setForeground(mBorderColor); + e.gc.drawRectangle( + leftOffset + mWidth - nodeTop - topShift - nodeHeight, topOffset + + leftShift + nodeLeft, nodeHeight, nodeWidth); + } else { + e.gc.setForeground(mPaddingColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft + nodePadLeft, + topOffset + topShift + nodeTop + nodePadTop, nodeWidth + - nodePadRight - nodePadLeft, nodeHeight + - nodePadBottom - nodePadTop); + e.gc.setForeground(mMarginColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft - nodeMarginLeft, + topOffset + topShift + nodeTop - nodeMarginTop, nodeWidth + + nodeMarginRight + nodeMarginLeft, nodeHeight + + nodeMarginBottom + nodeMarginTop); + e.gc.setForeground(mBorderColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft, topOffset + + topShift + nodeTop, nodeWidth, nodeHeight); + } + } + if (mCrosshairLocation != null) { + e.gc.setForeground(mCrosshairColor); + e.gc.drawLine(leftOffset, topOffset + mCrosshairLocation.y, leftOffset + + mWidth - 1, topOffset + mCrosshairLocation.y); + e.gc.drawLine(leftOffset + mCrosshairLocation.x, topOffset, leftOffset + + mCrosshairLocation.x, topOffset + mHeight - 1); + } + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mCanvas.redraw(); + } + }); + } + + private void loadImage() { + mImage = mModel.getImage(); + if (mImage != null) { + mWidth = mImage.getBounds().width; + mHeight = mImage.getBounds().height; + } else { + mWidth = 0; + mHeight = 0; + } + setMinSize(mWidth, mHeight); + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mSelectedNode = mModel.getSelected(); + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + } + }); + doRedraw(); + } + + @Override + public void imageChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + } + } + }); + doRedraw(); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelected(); + } + doRedraw(); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mSelectedNode = mModel.getSelected(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + // pass + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + synchronized (this) { + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java new file mode 100644 index 00000000..70e523b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Slider; + +public class PixelPerfectControls extends Composite implements IImageChangeListener { + + private Slider mOverlaySlider; + + private Slider mZoomSlider; + + private Slider mAutoRefreshSlider; + + public PixelPerfectControls(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FormLayout()); + + Label overlayTransparencyRight = new Label(this, SWT.NONE); + overlayTransparencyRight.setText("100%"); + FormData overlayTransparencyRightData = new FormData(); + overlayTransparencyRightData.right = new FormAttachment(100, -2); + overlayTransparencyRightData.top = new FormAttachment(0, 2); + overlayTransparencyRight.setLayoutData(overlayTransparencyRightData); + + Label refreshRight = new Label(this, SWT.NONE); + refreshRight.setText("40s"); + FormData refreshRightData = new FormData(); + refreshRightData.right = new FormAttachment(100, -2); + refreshRightData.top = new FormAttachment(overlayTransparencyRight, 2); + refreshRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); + refreshRight.setLayoutData(refreshRightData); + + Label zoomRight = new Label(this, SWT.NONE); + zoomRight.setText("24x"); + FormData zoomRightData = new FormData(); + zoomRightData.right = new FormAttachment(100, -2); + zoomRightData.top = new FormAttachment(refreshRight, 2); + zoomRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); + zoomRight.setLayoutData(zoomRightData); + + Label overlayTransparency = new Label(this, SWT.NONE); + Label refresh = new Label(this, SWT.NONE); + + overlayTransparency.setText("Overlay:"); + FormData overlayTransparencyData = new FormData(); + overlayTransparencyData.left = new FormAttachment(0, 2); + overlayTransparencyData.top = new FormAttachment(0, 2); + overlayTransparencyData.right = new FormAttachment(refresh, 0, SWT.RIGHT); + overlayTransparency.setLayoutData(overlayTransparencyData); + + refresh.setText("Refresh Rate:"); + FormData refreshData = new FormData(); + refreshData.top = new FormAttachment(overlayTransparency, 2); + refreshData.left = new FormAttachment(0, 2); + refresh.setLayoutData(refreshData); + + Label zoom = new Label(this, SWT.NONE); + zoom.setText("Zoom:"); + FormData zoomData = new FormData(); + zoomData.right = new FormAttachment(refresh, 0, SWT.RIGHT); + zoomData.top = new FormAttachment(refresh, 2); + zoomData.left = new FormAttachment(0, 2); + zoom.setLayoutData(zoomData); + + Label overlayTransparencyLeft = new Label(this, SWT.RIGHT); + overlayTransparencyLeft.setText("0%"); + FormData overlayTransparencyLeftData = new FormData(); + overlayTransparencyLeftData.top = new FormAttachment(0, 2); + overlayTransparencyLeftData.left = new FormAttachment(overlayTransparency, 2); + overlayTransparencyLeft.setLayoutData(overlayTransparencyLeftData); + + Label refreshLeft = new Label(this, SWT.RIGHT); + refreshLeft.setText("1s"); + FormData refreshLeftData = new FormData(); + refreshLeftData.top = new FormAttachment(overlayTransparencyLeft, 2); + refreshLeftData.left = new FormAttachment(refresh, 2); + refreshLeft.setLayoutData(refreshLeftData); + + Label zoomLeft = new Label(this, SWT.RIGHT); + zoomLeft.setText("2x"); + FormData zoomLeftData = new FormData(); + zoomLeftData.top = new FormAttachment(refreshLeft, 2); + zoomLeftData.left = new FormAttachment(zoom, 2); + zoomLeft.setLayoutData(zoomLeftData); + + mOverlaySlider = new Slider(this, SWT.HORIZONTAL); + mOverlaySlider.setMinimum(0); + mOverlaySlider.setMaximum(101); + mOverlaySlider.setThumb(1); + mOverlaySlider.setSelection((int) Math.round(PixelPerfectModel.getModel() + .getOverlayTransparency() * 100)); + + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + FormData overlaySliderData = new FormData(); + overlaySliderData.right = new FormAttachment(overlayTransparencyRight, -4); + overlaySliderData.top = new FormAttachment(0, 2); + overlaySliderData.left = new FormAttachment(overlayTransparencyLeft, 4); + mOverlaySlider.setLayoutData(overlaySliderData); + + mOverlaySlider.addSelectionListener(overlaySliderSelectionListener); + + mAutoRefreshSlider = new Slider(this, SWT.HORIZONTAL); + mAutoRefreshSlider.setMinimum(1); + mAutoRefreshSlider.setMaximum(41); + mAutoRefreshSlider.setThumb(1); + mAutoRefreshSlider.setSelection(HierarchyViewerDirector.getDirector() + .getPixelPerfectAutoRefreshInverval()); + FormData refreshSliderData = new FormData(); + refreshSliderData.right = new FormAttachment(overlayTransparencyRight, -4); + refreshSliderData.top = new FormAttachment(overlayTransparencyRight, 2); + refreshSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); + mAutoRefreshSlider.setLayoutData(refreshSliderData); + + mAutoRefreshSlider.addSelectionListener(mRefreshSliderSelectionListener); + + mZoomSlider = new Slider(this, SWT.HORIZONTAL); + mZoomSlider.setMinimum(2); + mZoomSlider.setMaximum(25); + mZoomSlider.setThumb(1); + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + FormData zoomSliderData = new FormData(); + zoomSliderData.right = new FormAttachment(overlayTransparencyRight, -4); + zoomSliderData.top = new FormAttachment(refreshRight, 2); + zoomSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); + mZoomSlider.setLayoutData(zoomSliderData); + + mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); + + addDisposeListener(mDisposeListener); + + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + } + }; + + private SelectionListener overlaySliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mOverlaySlider.getSelection(); + if (oldValue != newValue) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + PixelPerfectModel.getModel().setOverlayTransparency(newValue / 100.0); + PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); + oldValue = newValue; + } + } + }; + + private SelectionListener mRefreshSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(final SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mAutoRefreshSlider.getSelection(); + if (oldValue != newValue) { + HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefreshInterval(newValue); + } + } + }; + + private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mZoomSlider.getSelection(); + if (oldValue != newValue) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + PixelPerfectModel.getModel().setZoom(newValue); + PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); + oldValue = newValue; + } + } + }; + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void imageChanged() { + // pass + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + if (PixelPerfectModel.getModel().getImage() == null) { + } else { + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + } + } + }); + } + + @Override + public void overlayChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + } + }); + } + + @Override + public void overlayTransparencyChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mOverlaySlider.setSelection((int) (PixelPerfectModel.getModel() + .getOverlayTransparency() * 100)); + } + }); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java new file mode 100644 index 00000000..ee7cf483 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectLoupe extends Canvas implements IImageChangeListener { + private PixelPerfectModel mModel; + + private Image mImage; + + private Image mGrid; + + private Color mCrosshairColor; + + private int mWidth; + + private int mHeight; + + private Point mCrosshairLocation; + + private int mZoom; + + private Transform mTransform; + + private int mCanvasWidth; + + private int mCanvasHeight; + + private Image mOverlayImage; + + private double mOverlayTransparency; + + private boolean mShowOverlay = false; + + public PixelPerfectLoupe(Composite parent) { + super(parent, SWT.NONE); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseWheelListener(mMouseWheelListener); + addDisposeListener(mDisposeListener); + addKeyListener(mKeyListener); + + mCrosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254)); + + mTransform = new Transform(Display.getDefault()); + + imageLoaded(); + } + + public void setShowOverlay(boolean value) { + synchronized (this) { + mShowOverlay = value; + } + doRedraw(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectLoupe.this); + mCrosshairColor.dispose(); + mTransform.dispose(); + if (mGrid != null) { + mGrid.dispose(); + } + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + handleMouseEvent(e); + } + + @Override + public void mouseUp(MouseEvent e) { + // + } + + }; + + private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent e) { + int newZoom = -1; + synchronized (PixelPerfectLoupe.this) { + if (mImage != null && mCrosshairLocation != null) { + if (e.count > 0) { + newZoom = mZoom + 1; + } else { + newZoom = mZoom - 1; + } + } + } + if (newZoom != -1) { + mModel.setZoom(newZoom); + } + } + }; + + private void handleMouseEvent(MouseEvent e) { + int newX = -1; + int newY = -1; + synchronized (PixelPerfectLoupe.this) { + if (mImage == null) { + return; + } + int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; + int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; + int x = (e.x - zoomedX) / mZoom; + int y = (e.y - zoomedY) / mZoom; + if (x >= 0 && x < mWidth && y >= 0 && y < mHeight) { + newX = x; + newY = y; + } + } + if (newX != -1) { + mModel.setCrosshairLocation(newX, newY); + } + } + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean crosshairMoved = false; + synchronized (PixelPerfectLoupe.this) { + if (mImage != null) { + switch (e.keyCode) { + case SWT.ARROW_UP: + if (mCrosshairLocation.y != 0) { + mCrosshairLocation.y--; + crosshairMoved = true; + } + break; + case SWT.ARROW_DOWN: + if (mCrosshairLocation.y != mHeight - 1) { + mCrosshairLocation.y++; + crosshairMoved = true; + } + break; + case SWT.ARROW_LEFT: + if (mCrosshairLocation.x != 0) { + mCrosshairLocation.x--; + crosshairMoved = true; + } + break; + case SWT.ARROW_RIGHT: + if (mCrosshairLocation.x != mWidth - 1) { + mCrosshairLocation.x++; + crosshairMoved = true; + } + break; + } + } + } + if (crosshairMoved) { + mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); + } + } + + @Override + public void keyReleased(KeyEvent e) { + // pass + } + + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfectLoupe.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getSize().x, getSize().y); + if (mImage != null && mCrosshairLocation != null) { + int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; + int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; + mTransform.translate(zoomedX, zoomedY); + mTransform.scale(mZoom, mZoom); + e.gc.setInterpolation(SWT.NONE); + e.gc.setTransform(mTransform); + e.gc.drawImage(mImage, 0, 0); + if (mShowOverlay && mOverlayImage != null) { + e.gc.setAlpha((int) (mOverlayTransparency * 255)); + e.gc.drawImage(mOverlayImage, 0, mHeight - mOverlayImage.getBounds().height); + e.gc.setAlpha(255); + } + + mTransform.identity(); + e.gc.setTransform(mTransform); + + // If the size of the canvas has changed, we need to make + // another grid. + if (mGrid != null + && (mCanvasWidth != getBounds().width || mCanvasHeight != getBounds().height)) { + mGrid.dispose(); + mGrid = null; + } + mCanvasWidth = getBounds().width; + mCanvasHeight = getBounds().height; + if (mGrid == null) { + // Make a transparent image; + ImageData imageData = + new ImageData(mCanvasWidth + mZoom + 1, mCanvasHeight + mZoom + 1, 1, + new PaletteData(new RGB[] { + new RGB(0, 0, 0) + })); + imageData.transparentPixel = 0; + + // Draw the grid. + mGrid = new Image(Display.getDefault(), imageData); + GC gc = new GC(mGrid); + gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + for (int x = 0; x <= mCanvasWidth + mZoom; x += mZoom) { + gc.drawLine(x, 0, x, mCanvasHeight + mZoom); + } + for (int y = 0; y <= mCanvasHeight + mZoom; y += mZoom) { + gc.drawLine(0, y, mCanvasWidth + mZoom, y); + } + gc.dispose(); + } + + e.gc.setClipping(new Rectangle(zoomedX, zoomedY, mWidth * mZoom + 1, mHeight + * mZoom + 1)); + e.gc.setAlpha(76); + e.gc.drawImage(mGrid, (mCanvasWidth / 2 - mZoom / 2) % mZoom - mZoom, + (mCanvasHeight / 2 - mZoom / 2) % mZoom - mZoom); + e.gc.setAlpha(255); + + e.gc.setForeground(mCrosshairColor); + e.gc.drawLine(0, mCanvasHeight / 2, mCanvasWidth - 1, mCanvasHeight / 2); + e.gc.drawLine(mCanvasWidth / 2, 0, mCanvasWidth / 2, mCanvasHeight - 1); + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + private void loadImage() { + mImage = mModel.getImage(); + if (mImage != null) { + mWidth = mImage.getBounds().width; + mHeight = mImage.getBounds().height; + } else { + mWidth = 0; + mHeight = 0; + } + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mZoom = mModel.getZoom(); + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + } + }); + doRedraw(); + } + + @Override + public void imageChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + } + } + }); + doRedraw(); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + if (mGrid != null) { + // To notify that the zoom level has changed, we get rid + // of the + // grid. + mGrid.dispose(); + mGrid = null; + } + mZoom = mModel.getZoom(); + } + } + }); + doRedraw(); + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + synchronized (this) { + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java new file mode 100644 index 00000000..01088edf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectPixelPanel extends Canvas implements IImageChangeListener { + private PixelPerfectModel mModel; + + private Image mImage; + + private Image mOverlayImage; + + private Point mCrosshairLocation; + + public static final int PREFERRED_WIDTH = 180; + + public static final int PREFERRED_HEIGHT = 52; + + public PixelPerfectPixelPanel(Composite parent) { + super(parent, SWT.NONE); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + addPaintListener(mPaintListener); + addDisposeListener(mDisposeListener); + + imageLoaded(); + } + + @Override + public Point computeSize(int wHint, int hHint, boolean changed) { + int height = PREFERRED_HEIGHT; + int width = (wHint == SWT.DEFAULT) ? PREFERRED_WIDTH : wHint; + return new Point(width, height); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectPixelPanel.this); + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfectPixelPanel.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mImage != null) { + RGB pixel = + mImage.getImageData().palette.getRGB(mImage.getImageData().getPixel( + mCrosshairLocation.x, mCrosshairLocation.y)); + Color rgbColor = new Color(Display.getDefault(), pixel); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.setBackground(rgbColor); + e.gc.drawRectangle(4, 4, 60, 30); + e.gc.fillRectangle(5, 5, 59, 29); + rgbColor.dispose(); + e.gc.drawText("#" + + Integer + .toHexString( + (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + + pixel.blue).substring(1), 4, 35, true); + e.gc.drawText("R:", 80, 4, true); + e.gc.drawText("G:", 80, 20, true); + e.gc.drawText("B:", 80, 35, true); + e.gc.drawText(Integer.toString(pixel.red), 97, 4, true); + e.gc.drawText(Integer.toString(pixel.green), 97, 20, true); + e.gc.drawText(Integer.toString(pixel.blue), 97, 35, true); + e.gc.drawText("X:", 132, 4, true); + e.gc.drawText("Y:", 132, 20, true); + e.gc.drawText(Integer.toString(mCrosshairLocation.x) + " px", 149, 4, true); + e.gc.drawText(Integer.toString(mCrosshairLocation.y) + " px", 149, 20, true); + + if (mOverlayImage != null) { + int xInOverlay = mCrosshairLocation.x; + int yInOverlay = + mCrosshairLocation.y + - (mImage.getBounds().height - mOverlayImage.getBounds().height); + if (xInOverlay >= 0 && yInOverlay >= 0 + && xInOverlay < mOverlayImage.getBounds().width + && yInOverlay < mOverlayImage.getBounds().height) { + pixel = + mOverlayImage.getImageData().palette.getRGB(mOverlayImage + .getImageData().getPixel(xInOverlay, yInOverlay)); + rgbColor = new Color(Display.getDefault(), pixel); + e.gc + .setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_WHITE)); + e.gc.setBackground(rgbColor); + e.gc.drawRectangle(204, 4, 60, 30); + e.gc.fillRectangle(205, 5, 59, 29); + rgbColor.dispose(); + e.gc.drawText("#" + + Integer.toHexString( + (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + + pixel.blue).substring(1), 204, 35, true); + e.gc.drawText("R:", 280, 4, true); + e.gc.drawText("G:", 280, 20, true); + e.gc.drawText("B:", 280, 35, true); + e.gc.drawText(Integer.toString(pixel.red), 297, 4, true); + e.gc.drawText(Integer.toString(pixel.green), 297, 20, true); + e.gc.drawText(Integer.toString(pixel.blue), 297, 35, true); + } + } + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void imageChanged() { + synchronized (this) { + mImage = mModel.getImage(); + } + doRedraw(); + } + + @Override + public void imageLoaded() { + synchronized (this) { + mImage = mModel.getImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mOverlayImage = mModel.getOverlayImage(); + } + doRedraw(); + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java new file mode 100644 index 00000000..a73f900f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeSelection; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; + +import java.util.List; + +public class PixelPerfectTree extends Composite implements IImageChangeListener, SelectionListener { + + private TreeViewer mTreeViewer; + + private Tree mTree; + + private PixelPerfectModel mModel; + + private Image mFolderImage; + + private Image mFileImage; + + private class ContentProvider implements ITreeContentProvider, ILabelProvider { + @Override + public Object[] getChildren(Object element) { + if (element instanceof ViewNode) { + List children = ((ViewNode) element).children; + return children.toArray(new ViewNode[children.size()]); + } + return null; + } + + @Override + public Object getParent(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).parent; + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).children.size() != 0; + } + return false; + } + + @Override + public Object[] getElements(Object element) { + if (element instanceof PixelPerfectModel) { + ViewNode viewNode = ((PixelPerfectModel) element).getViewNode(); + if (viewNode == null) { + return new Object[0]; + } + return new Object[] { + viewNode + }; + } + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getImage(Object element) { + if (element instanceof ViewNode) { + if (hasChildren(element)) { + return mFolderImage; + } + return mFileImage; + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).name; + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public PixelPerfectTree(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.SINGLE); + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + mTree = mTreeViewer.getTree(); + mTree.addSelectionListener(this); + + loadResources(); + + addDisposeListener(mDisposeListener); + + mModel = PixelPerfectModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mTreeViewer.setInput(mModel); + mModel.addImageChangeListener(this); + + } + + private void loadResources() { + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mFileImage = imageFactory.getImageByName("file.png"); + mFolderImage = imageFactory.getImageByName("folder.png"); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectTree.this); + } + }; + + @Override + public boolean setFocus() { + return mTree.setFocus(); + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + mTreeViewer.expandAll(); + } + }); + } + + @Override + public void imageChanged() { + // pass + } + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + imageLoaded(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + // To combat phantom selection... + if (((TreeSelection) mTreeViewer.getSelection()).isEmpty()) { + mModel.setSelected(null); + } else { + mModel.setSelected((ViewNode) e.item.getData()); + } + } + + @Override + public void zoomChanged() { + // pass + } + + @Override + public void overlayChanged() { + // pass + } + + @Override + public void overlayTransparencyChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java new file mode 100644 index 00000000..08efbe30 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.ViewNode.Property; +import com.android.hierarchyviewerlib.ui.DevicePropertyEditingSupport.PropertyType; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.ComboBoxCellEditor; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import java.util.ArrayList; +import java.util.Collection; + +public class PropertyViewer extends Composite implements ITreeChangeListener { + private static final String PROPERTY_GET_PREFIX = "get"; + private static final String EMPTY_ARGUMENT_LIST = "()"; + + private TreeViewModel mModel; + + private TreeViewer mTreeViewer; + private Tree mTree; + private TreeViewerColumn mValueColumn; + private PropertyValueEditingSupport mPropertyValueEditingSupport; + + private Image mImage; + + private DrawableViewNode mSelectedNode; + + @VisibleForTesting + static @NonNull String parseColumnTextName(@NonNull String name) { + int start = 0; + int end = name.length(); + + int index = name.indexOf(':'); + if (index != -1) { + start = index + 1; + } + + index = name.indexOf(PROPERTY_GET_PREFIX); + int prefixOffset = start + PROPERTY_GET_PREFIX.length(); + if (index == start && prefixOffset < end + && Character.isUpperCase(name.charAt(prefixOffset))) { + start = prefixOffset; + } + + if (name.endsWith(EMPTY_ARGUMENT_LIST)) { + end -= EMPTY_ARGUMENT_LIST.length(); + } + + if (start < end && !Character.isLowerCase(name.charAt(start))) { + return Character.toLowerCase(name.charAt(start)) + + (start + 1 < end ? name.substring(start + 1, end) : ""); + } + + return name.substring(start, end); + } + + private class ContentProvider implements ITreeContentProvider, ITableLabelProvider { + + @Override + public Object[] getChildren(Object parentElement) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && parentElement instanceof String) { + String category = (String) parentElement; + ArrayList returnValue = new ArrayList(); + for (Property property : mSelectedNode.viewNode.properties) { + if (category.equals(ViewNode.MISCELLANIOUS)) { + if (property.name.indexOf(':') == -1) { + returnValue.add(property); + } + } else { + if (property.name.startsWith(((String) parentElement) + ":")) { + returnValue.add(property); + } + } + } + return returnValue.toArray(new Property[returnValue.size()]); + } + return new Object[0]; + } + } + + @Override + public Object getParent(Object element) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && element instanceof Property) { + if (mSelectedNode.viewNode.categories.size() == 0) { + return null; + } + String name = ((Property) element).name; + int index = name.indexOf(':'); + if (index == -1) { + return ViewNode.MISCELLANIOUS; + } + return name.substring(0, index); + } + return null; + } + } + + @Override + public boolean hasChildren(Object element) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && element instanceof String) { + String category = (String) element; + for (String name : mSelectedNode.viewNode.namedProperties.keySet()) { + if (category.equals(ViewNode.MISCELLANIOUS)) { + if (name.indexOf(':') == -1) { + return true; + } + } else { + if (name.startsWith(((String) element) + ":")) { + return true; + } + } + } + } + return false; + } + } + + @Override + public Object[] getElements(Object inputElement) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && inputElement instanceof TreeViewModel) { + if (mSelectedNode.viewNode.categories.size() == 0) { + return mSelectedNode.viewNode.properties + .toArray(new Property[mSelectedNode.viewNode.properties.size()]); + } else { + return mSelectedNode.viewNode.categories + .toArray(new String[mSelectedNode.viewNode.categories.size()]); + } + } + return new Object[0]; + } + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getColumnImage(Object element, int column) { + if (mSelectedNode == null) { + return null; + } + if (column == 1 && mPropertyValueEditingSupport.canEdit(element)) { + return mImage; + } + + return null; + } + + @Override + public String getColumnText(Object element, int column) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null) { + if (element instanceof String && column == 0) { + String category = (String) element; + return Character.toUpperCase(category.charAt(0)) + category.substring(1); + } else if (element instanceof Property) { + if (column == 0) { + return parseColumnTextName(((Property) element).name); + } else if (column == 1) { + return ((Property) element).value; + } + } + } + return ""; + } + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + private class PropertyValueEditingSupport extends EditingSupport { + private DevicePropertyEditingSupport mDevicePropertyEditingSupport = + new DevicePropertyEditingSupport(); + + public PropertyValueEditingSupport(ColumnViewer viewer) { + super(viewer); + } + + @Override + protected boolean canEdit(Object element) { + if (mSelectedNode == null) { + return false; + } + + return element instanceof Property + && mSelectedNode.viewNode.window.getHvDevice().isViewUpdateEnabled() + && mDevicePropertyEditingSupport.canEdit((Property) element); + } + + @Override + protected CellEditor getCellEditor(Object element) { + Property p = (Property) element; + PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); + Composite parent = (Composite) getViewer().getControl(); + + switch (type) { + case INTEGER: + case INTEGER_OR_CONSTANT: + return new TextCellEditor(parent); + case ENUM: + String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); + return new ComboBoxCellEditor(parent, items, SWT.READ_ONLY); + } + + return null; + } + + @Override + protected Object getValue(Object element) { + Property p = (Property) element; + PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); + + if (type == PropertyType.ENUM) { + // for enums, return the index of the current value in the list of possible values + String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); + return Integer.valueOf(indexOf(p.value, items)); + } + + return ((Property) element).value; + } + + private int indexOf(String item, String[] items) { + for (int i = 0; i < items.length; i++) { + if (items[i].equals(item)) { + return i; + } + } + + return -1; + } + + @Override + protected void setValue(Object element, Object newValue) { + Property p = (Property) element; + IHvDevice device = mSelectedNode.viewNode.window.getHvDevice(); + Collection properties = mSelectedNode.viewNode.namedProperties.values(); + if (mDevicePropertyEditingSupport.setValue(properties, p, newValue, + mSelectedNode.viewNode, device)) { + doRefresh(); + } + } + } + + public PropertyViewer(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.NONE); + + mTree = mTreeViewer.getTree(); + mTree.setLinesVisible(true); + mTree.setHeaderVisible(true); + + TreeColumn propertyColumn = new TreeColumn(mTree, SWT.NONE); + propertyColumn.setText("Property"); + TreeColumn valueColumn = new TreeColumn(mTree, SWT.NONE); + valueColumn.setText("Value"); + + mValueColumn = new TreeViewerColumn(mTreeViewer, valueColumn); + mPropertyValueEditingSupport = new PropertyValueEditingSupport(mTreeViewer); + mValueColumn.setEditingSupport(mPropertyValueEditingSupport); + + mModel = TreeViewModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mTreeViewer.setInput(mModel); + mModel.addTreeChangeListener(this); + + addDisposeListener(mDisposeListener); + + @SuppressWarnings("unused") + TreeColumnResizer resizer = new TreeColumnResizer(this, propertyColumn, valueColumn); + + addControlListener(mControlListener); + + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("picker.png"); //$NON-NLS-1$ + + treeChanged(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(PropertyViewer.this); + } + }; + + // If the window gets too small, hide the data, otherwise SWT throws an + // ERROR. + + private ControlListener mControlListener = new ControlAdapter() { + private boolean noInput = false; + + private boolean noHeader = false; + + @Override + public void controlResized(ControlEvent e) { + if (getBounds().height <= 20) { + mTree.setHeaderVisible(false); + noHeader = true; + } else if (noHeader) { + mTree.setHeaderVisible(true); + noHeader = false; + } + if (getBounds().height <= 38) { + mTreeViewer.setInput(null); + noInput = true; + } else if (noInput) { + mTreeViewer.setInput(mModel); + noInput = false; + } + } + }; + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRefresh(); + } + + @Override + public void treeChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRefresh(); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } + + private void doRefresh() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java new file mode 100644 index 00000000..4fa5691f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java @@ -0,0 +1,1096 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode.ProfileRating; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +import java.text.DecimalFormat; + +public class TreeView extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Rectangle mViewport; + + private Transform mTransform; + + private Transform mInverse; + + private double mZoom; + + private Point mLastPoint; + + private boolean mAlreadySelectedOnMouseDown; + + private boolean mDoubleClicked; + + private boolean mNodeMoved; + + private DrawableViewNode mDraggedNode; + + public static final int LINE_PADDING = 10; + + public static final float BEZIER_FRACTION = 0.35f; + + private static Image sRedImage; + + private static Image sYellowImage; + + private static Image sGreenImage; + + private static Image sNotSelectedImage; + + private static Image sSelectedImage; + + private static Image sFilteredImage; + + private static Image sFilteredSelectedImage; + + private static Font sSystemFont; + + private Color mBoxColor; + + private Color mTextBackgroundColor; + + private Rectangle mSelectedRectangleLocation; + + private Point mButtonCenter; + + private static final int BUTTON_SIZE = 13; + + private Image mScaledSelectedImage; + + private boolean mButtonClicked; + + private DrawableViewNode mLastDrawnSelectedViewNode; + + // The profile-image box needs to be moved to, + // so add some dragging leeway. + private static final int DRAG_LEEWAY = 220; + + // Profile-image box constants + private static final int RECT_WIDTH = 190; + + private static final int RECT_HEIGHT = 224; + + private static final int BUTTON_RIGHT_OFFSET = 5; + + private static final int BUTTON_TOP_OFFSET = 5; + + private static final int IMAGE_WIDTH = 125; + + private static final int IMAGE_HEIGHT = 120; + + private static final int IMAGE_OFFSET = 6; + + private static final int IMAGE_ROUNDING = 8; + + private static final int RECTANGLE_SIZE = 5; + + private static final int TEXT_SIDE_OFFSET = 8; + + private static final int TEXT_TOP_OFFSET = 4; + + private static final int TEXT_SPACING = 2; + + private static final int TEXT_ROUNDING = 20; + + public TreeView(Composite parent) { + super(parent, SWT.NONE); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseMoveListener(mMouseMoveListener); + addMouseWheelListener(mMouseWheelListener); + addListener(SWT.Resize, mResizeListener); + addDisposeListener(mDisposeListener); + addKeyListener(mKeyListener); + addTraverseListener(new TraverseListener() { + @Override + public void keyTraversed(TraverseEvent e) { + if (e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) { + e.doit = true; + } + } + }); + + loadResources(); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + loadAllData(); + } + + private void loadResources() { + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sRedImage = imageFactory.getImageByName("red.png"); //$NON-NLS-1$ + sYellowImage = imageFactory.getImageByName("yellow.png"); //$NON-NLS-1$ + sGreenImage = imageFactory.getImageByName("green.png"); //$NON-NLS-1$ + sNotSelectedImage = imageFactory.getImageByName("not-selected.png"); //$NON-NLS-1$ + sSelectedImage = imageFactory.getImageByName("selected.png"); //$NON-NLS-1$ + sFilteredImage = imageFactory.getImageByName("filtered.png"); //$NON-NLS-1$ + sFilteredSelectedImage = imageFactory.getImageByName("selected-filtered.png"); //$NON-NLS-1$ + mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225)); + mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82)); + if (mScaledSelectedImage != null) { + mScaledSelectedImage.dispose(); + } + sSystemFont = Display.getDefault().getSystemFont(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(TreeView.this); + mTransform.dispose(); + mInverse.dispose(); + mBoxColor.dispose(); + mTextBackgroundColor.dispose(); + if (mTree != null) { + mModel.setViewport(null); + } + } + }; + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + + // Keep the center in the same place. + Point viewCenter = + new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height + / 2); + mViewport.width = getBounds().width / mZoom; + mViewport.height = getBounds().height / mZoom; + mViewport.x = viewCenter.x - mViewport.width / 2; + mViewport.y = viewCenter.y - mViewport.height / 2; + } + } + if (mViewport != null) { + mModel.setViewport(mViewport); + } + } + }; + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean selectionChanged = false; + DrawableViewNode clickedNode = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mSelectedNode != null) { + switch (e.keyCode) { + case SWT.ARROW_LEFT: + if (mSelectedNode.parent != null) { + mSelectedNode = mSelectedNode.parent; + selectionChanged = true; + } + break; + case SWT.ARROW_UP: + + // On up and down, it is cool to go up and down only + // the leaf nodes. + // It goes well with the layout viewer + DrawableViewNode currentNode = mSelectedNode; + while (currentNode.parent != null && currentNode.viewNode.index == 0) { + currentNode = currentNode.parent; + } + if (currentNode.parent != null) { + selectionChanged = true; + currentNode = + currentNode.parent.children + .get(currentNode.viewNode.index - 1); + while (currentNode.children.size() != 0) { + currentNode = + currentNode.children + .get(currentNode.children.size() - 1); + } + } + if (selectionChanged) { + mSelectedNode = currentNode; + } + break; + case SWT.ARROW_DOWN: + currentNode = mSelectedNode; + while (currentNode.parent != null + && currentNode.viewNode.index + 1 == currentNode.parent.children + .size()) { + currentNode = currentNode.parent; + } + if (currentNode.parent != null) { + selectionChanged = true; + currentNode = + currentNode.parent.children + .get(currentNode.viewNode.index + 1); + while (currentNode.children.size() != 0) { + currentNode = currentNode.children.get(0); + } + } + if (selectionChanged) { + mSelectedNode = currentNode; + } + break; + case SWT.ARROW_RIGHT: + DrawableViewNode rightNode = null; + double mostOverlap = 0; + final int N = mSelectedNode.children.size(); + + // We consider all the children and pick the one + // who's tree overlaps the most. + for (int i = 0; i < N; i++) { + DrawableViewNode child = mSelectedNode.children.get(i); + DrawableViewNode topMostChild = child; + while (topMostChild.children.size() != 0) { + topMostChild = topMostChild.children.get(0); + } + double overlap = + Math.min(DrawableViewNode.NODE_HEIGHT, Math.min( + mSelectedNode.top + DrawableViewNode.NODE_HEIGHT + - topMostChild.top, topMostChild.top + + child.treeHeight - mSelectedNode.top)); + if (overlap > mostOverlap) { + mostOverlap = overlap; + rightNode = child; + } + } + if (rightNode != null) { + mSelectedNode = rightNode; + selectionChanged = true; + } + break; + case SWT.CR: + clickedNode = mSelectedNode; + break; + } + } + } + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + if (clickedNode != null) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + DrawableViewNode clickedNode = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + Point pt = transformPoint(e.x, e.y); + clickedNode = mTree.getSelected(pt.x, pt.y); + } + } + if (clickedNode != null) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); + mDoubleClicked = true; + } + } + + @Override + public void mouseDown(MouseEvent e) { + boolean selectionChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + Point pt = transformPoint(e.x, e.y); + + // Ignore profiling rectangle, except for... + if (mSelectedRectangleLocation != null + && pt.x >= mSelectedRectangleLocation.x + && pt.x < mSelectedRectangleLocation.x + + mSelectedRectangleLocation.width + && pt.y >= mSelectedRectangleLocation.y + && pt.y < mSelectedRectangleLocation.y + + mSelectedRectangleLocation.height) { + + // the small button! + if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x) + + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) { + mButtonClicked = true; + doRedraw(); + } + return; + } + mDraggedNode = mTree.getSelected(pt.x, pt.y); + + // Update the selection. + if (mDraggedNode != null && mDraggedNode != mSelectedNode) { + mSelectedNode = mDraggedNode; + selectionChanged = true; + mAlreadySelectedOnMouseDown = false; + } else if (mDraggedNode != null) { + mAlreadySelectedOnMouseDown = true; + } + + // Can't drag the root. + if (mDraggedNode == mTree) { + mDraggedNode = null; + } + + if (mDraggedNode != null) { + mLastPoint = pt; + } else { + mLastPoint = new Point(e.x, e.y); + } + mNodeMoved = false; + mDoubleClicked = false; + } + } + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + } + + @Override + public void mouseUp(MouseEvent e) { + boolean redraw = false; + boolean redrawButton = false; + boolean viewportChanged = false; + boolean selectionChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mLastPoint != null) { + if (mDraggedNode == null) { + // The viewport moves. + handleMouseDrag(new Point(e.x, e.y)); + viewportChanged = true; + } else { + // The nodes move. + handleMouseDrag(transformPoint(e.x, e.y)); + } + + // Deselect on the second click... + // This is in the mouse up, because mouse up happens after a + // double click event. + // During a double click, we don't want to deselect. + Point pt = transformPoint(e.x, e.y); + DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y); + if (mouseUpOn != null && mouseUpOn == mSelectedNode + && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) { + mSelectedNode = null; + selectionChanged = true; + } + mLastPoint = null; + mDraggedNode = null; + redraw = true; + } + + // Just clicked the button here. + if (mButtonClicked) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), + mSelectedNode.viewNode); + mButtonClicked = false; + redrawButton = true; + } + } + + // Complicated. + if (viewportChanged) { + mModel.setViewport(mViewport); + } else if (redraw) { + mModel.removeTreeChangeListener(TreeView.this); + mModel.notifyViewportChanged(); + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + mModel.addTreeChangeListener(TreeView.this); + doRedraw(); + } else if (redrawButton) { + doRedraw(); + } + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + boolean redraw = false; + boolean viewportChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mLastPoint != null) { + if (mDraggedNode == null) { + handleMouseDrag(new Point(e.x, e.y)); + viewportChanged = true; + } else { + handleMouseDrag(transformPoint(e.x, e.y)); + } + redraw = true; + } + } + if (viewportChanged) { + mModel.setViewport(mViewport); + } else if (redraw) { + mModel.removeTreeChangeListener(TreeView.this); + mModel.notifyViewportChanged(); + mModel.addTreeChangeListener(TreeView.this); + doRedraw(); + } + } + }; + + private void handleMouseDrag(Point pt) { + + // Case 1: a node is dragged. DrawableViewNode knows how to handle this. + if (mDraggedNode != null) { + if (mLastPoint.y - pt.y != 0) { + mNodeMoved = true; + } + mDraggedNode.move(mLastPoint.y - pt.y); + mLastPoint = pt; + return; + } + + // Case 2: the viewport is dragged. We have to make sure we respect the + // bounds - don't let the user drag way out... + some leeway for the + // profiling box. + double xDif = (mLastPoint.x - pt.x) / mZoom; + double yDif = (mLastPoint.y - pt.y) / mZoom; + + double treeX = mTree.bounds.x - DRAG_LEEWAY; + double treeY = mTree.bounds.y - DRAG_LEEWAY; + double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY; + double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY; + + if (mViewport.width > treeWidth) { + if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) { + mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width); + } else if (xDif > 0 && mViewport.x < treeX) { + mViewport.x = Math.min(mViewport.x + xDif, treeX); + } + } else { + if (xDif < 0 && mViewport.x > treeX) { + mViewport.x = Math.max(mViewport.x + xDif, treeX); + } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) { + mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width); + } + } + if (mViewport.height > treeHeight) { + if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) { + mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height); + } else if (yDif > 0 && mViewport.y < treeY) { + mViewport.y = Math.min(mViewport.y + yDif, treeY); + } + } else { + if (yDif < 0 && mViewport.y > treeY) { + mViewport.y = Math.max(mViewport.y + yDif, treeY); + } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) { + mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height); + } + } + mLastPoint = pt; + } + + private Point transformPoint(double x, double y) { + float[] pt = { + (float) x, (float) y + }; + mInverse.transform(pt); + return new Point(pt[0], pt[1]); + } + + private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent e) { + Point zoomPoint = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + mZoom += Math.ceil(e.count / 3.0) * 0.1; + zoomPoint = transformPoint(e.x, e.y); + } + } + if (zoomPoint != null) { + mModel.zoomOnPoint(mZoom, zoomPoint); + } + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (TreeView.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mTree != null && mViewport != null) { + + // Easy stuff! + e.gc.setTransform(mTransform); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + Path connectionPath = new Path(Display.getDefault()); + paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath); + e.gc.drawPath(connectionPath); + connectionPath.dispose(); + + // Draw the profiling box. + if (mSelectedNode != null) { + + e.gc.setAlpha(200); + + // Draw the little triangle + int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2; + int y = (int) mSelectedNode.top + 4; + e.gc.setBackground(mBoxColor); + e.gc.fillPolygon(new int[] { + x, y, x - 11, y - 11, x + 11, y - 11 + }); + + // Draw the rectangle and update the location. + y -= 10 + RECT_HEIGHT; + e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30, + 30); + mSelectedRectangleLocation = + new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT); + + e.gc.setAlpha(255); + + // Draw the button + mButtonCenter = + new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2, + y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2); + + if (mButtonClicked) { + e.gc + .setBackground(Display.getDefault().getSystemColor( + SWT.COLOR_BLACK)); + } else { + e.gc.setBackground(mTextBackgroundColor); + + } + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + + e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y + + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + + e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET + + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y + + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2, + RECTANGLE_SIZE + 1, RECTANGLE_SIZE); + + y += 15; + + // If there is an image, draw it. + if (mSelectedNode.viewNode.image != null + && mSelectedNode.viewNode.image.getBounds().height != 1 + && mSelectedNode.viewNode.image.getBounds().width != 1) { + + // Scaling the image to the right size takes lots of + // time, so we want to do it only once. + + // If the selection changed, get rid of the old + // image. + if (mLastDrawnSelectedViewNode != mSelectedNode) { + if (mScaledSelectedImage != null) { + mScaledSelectedImage.dispose(); + mScaledSelectedImage = null; + } + mLastDrawnSelectedViewNode = mSelectedNode; + } + + if (mScaledSelectedImage == null) { + double ratio = + 1.0 * mSelectedNode.viewNode.image.getBounds().width + / mSelectedNode.viewNode.image.getBounds().height; + int newWidth, newHeight; + if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) { + newWidth = + Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image + .getBounds().width); + newHeight = (int) (newWidth / ratio); + } else { + newHeight = + Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image + .getBounds().height); + newWidth = (int) (newHeight * ratio); + } + + // Interesting note... We make the image twice + // the needed size so that there is better + // resolution under zoom. + newWidth = Math.max(newWidth * 2, 1); + newHeight = Math.max(newHeight * 2, 1); + mScaledSelectedImage = + new Image(Display.getDefault(), newWidth, newHeight); + GC gc = new GC(mScaledSelectedImage); + gc.setBackground(mTextBackgroundColor); + gc.fillRectangle(0, 0, newWidth, newHeight); + gc.drawImage(mSelectedNode.viewNode.image, 0, 0, + mSelectedNode.viewNode.image.getBounds().width, + mSelectedNode.viewNode.image.getBounds().height, 0, 0, + newWidth, newHeight); + gc.dispose(); + } + + // Draw the background rectangle + e.gc.setBackground(mTextBackgroundColor); + e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4 + - IMAGE_OFFSET, y + + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) + / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2 + + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2 + + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING); + + // Under max zoom, we want the image to be + // untransformed. So, get back to the identity + // transform. + int imageX = x - mScaledSelectedImage.getBounds().width / 4; + int imageY = + y + + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) + / 2; + + Transform untransformedTransform = new Transform(Display.getDefault()); + e.gc.setTransform(untransformedTransform); + float[] pt = new float[] { + imageX, imageY + }; + mTransform.transform(pt); + e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage + .getBounds().width, mScaledSelectedImage.getBounds().height, + (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage + .getBounds().width + * mZoom / 2), + (int) (mScaledSelectedImage.getBounds().height * mZoom / 2)); + untransformedTransform.dispose(); + e.gc.setTransform(mTransform); + } + + // Text stuff + + y += IMAGE_HEIGHT; + y += 10; + Font font = getFont(8, false); + e.gc.setFont(font); + + String text = + mSelectedNode.viewNode.viewCount + " view" + + (mSelectedNode.viewNode.viewCount != 1 ? "s" : ""); + DecimalFormat formatter = new DecimalFormat("0.000"); + + String measureText = + "Measure: " + + (mSelectedNode.viewNode.measureTime != -1 ? formatter + .format(mSelectedNode.viewNode.measureTime) + + " ms" : "n/a"); + String layoutText = + "Layout: " + + (mSelectedNode.viewNode.layoutTime != -1 ? formatter + .format(mSelectedNode.viewNode.layoutTime) + + " ms" : "n/a"); + String drawText = + "Draw: " + + (mSelectedNode.viewNode.drawTime != -1 ? formatter + .format(mSelectedNode.viewNode.drawTime) + + " ms" : "n/a"); + + org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text); + org.eclipse.swt.graphics.Point measureExtent = + e.gc.stringExtent(measureText); + org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText); + org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText); + int boxWidth = + Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max( + layoutExtent.x, drawExtent.x))) + + 2 * TEXT_SIDE_OFFSET; + int boxHeight = + titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING + + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2 + * TEXT_TOP_OFFSET; + + e.gc.setBackground(mTextBackgroundColor); + e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight, + TEXT_ROUNDING, TEXT_ROUNDING); + + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + + y += TEXT_TOP_OFFSET; + + e.gc.drawText(text, x - titleExtent.x / 2, y, true); + + x -= boxWidth / 2; + x += TEXT_SIDE_OFFSET; + + y += titleExtent.y + TEXT_SPACING; + + e.gc.drawText(measureText, x, y, true); + + y += measureExtent.y + TEXT_SPACING; + + e.gc.drawText(layoutText, x, y, true); + + y += layoutExtent.y + TEXT_SPACING; + + e.gc.drawText(drawText, x, y, true); + + font.dispose(); + } else { + mSelectedRectangleLocation = null; + mButtonCenter = null; + } + } + } + } + }; + + private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node, + DrawableViewNode selectedNode, Path connectionPath) { + if (selectedNode == node && node.viewNode.filtered) { + gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); + } else if (selectedNode == node) { + gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); + } else if (node.viewNode.filtered) { + gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); + } else { + gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); + } + + int fontHeight = gc.getFontMetrics().getHeight(); + + // Draw the text... + int contentWidth = + DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; + String name = node.viewNode.name; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex != -1) { + name = name.substring(dotIndex + 1); + } + double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; + double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING; + drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true); + + y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; + + drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight, + 8, false); + + y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; + if (!node.viewNode.id.equals("NO_ID")) { + drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8, + false); + } + + if (node.viewNode.measureRating != ProfileRating.NONE) { + y = + node.top + DrawableViewNode.NODE_HEIGHT + - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING + - sRedImage.getBounds().height; + x += + (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2; + switch (node.viewNode.measureRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + + x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; + switch (node.viewNode.layoutRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + + x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; + switch (node.viewNode.drawRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + } + + org.eclipse.swt.graphics.Point indexExtent = + gc.stringExtent(Integer.toString(node.viewNode.index)); + x = + node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING + - indexExtent.x; + y = + node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING + - indexExtent.y; + gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT); + + int N = node.children.size(); + if (N == 0) { + return; + } + float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N; + for (int i = 0; i < N; i++) { + DrawableViewNode child = node.children.get(i); + paintRecursive(gc, transform, child, selectedNode, connectionPath); + float x1 = node.left + DrawableViewNode.NODE_WIDTH; + float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2; + float x2 = child.left; + float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; + float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy1 = y1; + float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy2 = y2; + connectionPath.moveTo(x1, y1); + connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); + } + } + + private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y, + double width, double height, int fontSize, boolean bold) { + + Font oldFont = gc.getFont(); + + Font newFont = getFont(fontSize, bold); + gc.setFont(newFont); + + org.eclipse.swt.graphics.Point extent = gc.stringExtent(text); + + if (extent.x > width) { + // Oh no... we need to scale it. + double scale = width / extent.x; + float[] transformElements = new float[6]; + transform.getElements(transformElements); + transform.scale((float) scale, (float) scale); + gc.setTransform(transform); + + x /= scale; + y /= scale; + y += (extent.y / scale - extent.y) / 2; + + gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT); + + transform.setElements(transformElements[0], transformElements[1], transformElements[2], + transformElements[3], transformElements[4], transformElements[5]); + gc.setTransform(transform); + } else { + gc.drawText(text, (int) (x + (width - extent.x) / 2), + (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT); + } + gc.setFont(oldFont); + newFont.dispose(); + + } + + public static Image paintToImage(DrawableViewNode tree) { + Image image = + new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math + .ceil(tree.bounds.height)); + + Transform transform = new Transform(Display.getDefault()); + transform.identity(); + transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y); + Path connectionPath = new Path(Display.getDefault()); + GC gc = new GC(image); + + // Can't use Display.getDefault().getSystemColor in a non-UI thread. + Color white = new Color(Display.getDefault(), 255, 255, 255); + Color black = new Color(Display.getDefault(), 0, 0, 0); + gc.setForeground(white); + gc.setBackground(black); + gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height); + gc.setTransform(transform); + paintRecursive(gc, transform, tree, null, connectionPath); + gc.drawPath(connectionPath); + gc.dispose(); + connectionPath.dispose(); + white.dispose(); + black.dispose(); + return image; + } + + private static Font getFont(int size, boolean bold) { + FontData[] fontData = sSystemFont.getFontData(); + for (int i = 0; i < fontData.length; i++) { + fontData[i].setHeight(size); + if (bold) { + fontData[i].setStyle(SWT.BOLD); + } + } + return new Font(Display.getDefault(), fontData); + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + public void loadAllData() { + boolean newViewport = mViewport == null; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + mZoom = mModel.getZoom(); + if (mTree != null && mViewport == null) { + mViewport = + new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 + - getBounds().height / 2, getBounds().width, + getBounds().height); + } else { + setTransform(); + } + } + } + }); + if (newViewport) { + mModel.setViewport(mViewport); + } + } + + // Fickle behaviour... When a new tree is loaded, the model doesn't know + // about the viewport until it passes through here. + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + if (mTree == null) { + mViewport = null; + } else { + mViewport = + new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 + - getBounds().height / 2, getBounds().width, + getBounds().height); + } + } + } + }); + if (mViewport != null) { + mModel.setViewport(mViewport); + } else { + doRedraw(); + } + } + + private void setTransform() { + if (mViewport != null && mTree != null) { + // Set the transform. + mTransform.identity(); + mInverse.identity(); + + mTransform.scale((float) mZoom, (float) mZoom); + mInverse.scale((float) mZoom, (float) mZoom); + mTransform.translate((float) -mViewport.x, (float) -mViewport.y); + mInverse.translate((float) -mViewport.x, (float) -mViewport.y); + mInverse.invert(); + } + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void viewportChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mViewport = mModel.getViewport(); + mZoom = mModel.getZoom(); + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + viewportChanged(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null && mSelectedNode.viewNode.image == null) { + HierarchyViewerDirector.getDirector() + .loadCaptureInBackground(mSelectedNode.viewNode); + } + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java new file mode 100644 index 00000000..0b4dab44 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Slider; +import org.eclipse.swt.widgets.Text; + +public class TreeViewControls extends Composite implements ITreeChangeListener { + + private Text mFilterText; + + private Slider mZoomSlider; + + public TreeViewControls(Composite parent) { + super(parent, SWT.NONE); + GridLayout layout = new GridLayout(5, false); + layout.marginWidth = layout.marginHeight = 2; + layout.verticalSpacing = layout.horizontalSpacing = 4; + setLayout(layout); + + Label filterLabel = new Label(this, SWT.NONE); + filterLabel.setText("Filter by class or id:"); + filterLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + + mFilterText = new Text(this, SWT.LEFT | SWT.SINGLE); + mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterText.addModifyListener(mFilterTextModifyListener); + mFilterText.setText(HierarchyViewerDirector.getDirector().getFilterText()); + + Label smallZoomLabel = new Label(this, SWT.NONE); + smallZoomLabel.setText(" 20%"); + smallZoomLabel + .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + + mZoomSlider = new Slider(this, SWT.HORIZONTAL); + GridData zoomSliderGridData = new GridData(GridData.CENTER, GridData.CENTER, false, false); + zoomSliderGridData.widthHint = 190; + mZoomSlider.setLayoutData(zoomSliderGridData); + mZoomSlider.setMinimum((int) (TreeViewModel.MIN_ZOOM * 10)); + mZoomSlider.setMaximum((int) (TreeViewModel.MAX_ZOOM * 10 + 1)); + mZoomSlider.setThumb(1); + mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); + + mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); + + Label largeZoomLabel = new Label(this, SWT.NONE); + largeZoomLabel + .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + largeZoomLabel.setText("200%"); + + addDisposeListener(mDisposeListener); + + TreeViewModel.getModel().addTreeChangeListener(this); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); + } + }; + + private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mZoomSlider.getSelection(); + if (oldValue != newValue) { + TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); + TreeViewModel.getModel().setZoom(newValue / 10.0); + TreeViewModel.getModel().addTreeChangeListener(TreeViewControls.this); + oldValue = newValue; + } + } + }; + + private ModifyListener mFilterTextModifyListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + HierarchyViewerDirector.getDirector().filterNodes(mFilterText.getText()); + } + }; + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + if (TreeViewModel.getModel().getTree() != null) { + mZoomSlider.setSelection((int) Math + .round(TreeViewModel.getModel().getZoom() * 10)); + } + mFilterText.setText(""); //$NON-NLS-1$ + } + }); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); + } + }); + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java new file mode 100644 index 00000000..87181a3c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +public class TreeViewOverview extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private Rectangle mViewport; + + private Transform mTransform; + + private Transform mInverse; + + private Rectangle mBounds = new Rectangle(); + + private double mScale; + + private boolean mDragging = false; + + private DrawableViewNode mSelectedNode; + + private static Image sNotSelectedImage; + + private static Image sSelectedImage; + + private static Image sFilteredImage; + + private static Image sFilteredSelectedImage; + + public TreeViewOverview(Composite parent) { + super(parent, SWT.NONE); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + loadResources(); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseMoveListener(mMouseMoveListener); + addListener(SWT.Resize, mResizeListener); + addDisposeListener(mDisposeListener); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + loadAllData(); + } + + private void loadResources() { + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sNotSelectedImage = imageFactory.getImageByName("not-selected.png"); //$NON-NLS-1$ + sSelectedImage = imageFactory.getImageByName("selected-small.png"); //$NON-NLS-1$ + sFilteredImage = imageFactory.getImageByName("filtered.png"); //$NON-NLS-1$ + sFilteredSelectedImage = + imageFactory.getImageByName("selected-filtered-small.png"); //$NON-NLS-1$ + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mTransform.dispose(); + mInverse.dispose(); + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + boolean redraw = false; + synchronized (TreeViewOverview.this) { + if (mTree != null && mViewport != null) { + mDragging = true; + redraw = true; + handleMouseEvent(transformPoint(e.x, e.y)); + } + } + if (redraw) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + + @Override + public void mouseUp(MouseEvent e) { + boolean redraw = false; + synchronized (TreeViewOverview.this) { + if (mTree != null && mViewport != null) { + mDragging = false; + redraw = true; + handleMouseEvent(transformPoint(e.x, e.y)); + + // Update bounds and transform only on mouse up. That way, + // you don't get confusing behaviour during mouse drag and + // it snaps neatly at the end + setBounds(); + setTransform(); + } + } + if (redraw) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + boolean moved = false; + synchronized (TreeViewOverview.this) { + if (mDragging) { + moved = true; + handleMouseEvent(transformPoint(e.x, e.y)); + } + } + if (moved) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + }; + + private void handleMouseEvent(Point pt) { + mViewport.x = pt.x - mViewport.width / 2; + mViewport.y = pt.y - mViewport.height / 2; + if (mViewport.x < mBounds.x) { + mViewport.x = mBounds.x; + } + if (mViewport.y < mBounds.y) { + mViewport.y = mBounds.y; + } + if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) { + mViewport.x = mBounds.x + mBounds.width - mViewport.width; + } + if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) { + mViewport.y = mBounds.y + mBounds.height - mViewport.height; + } + } + + private Point transformPoint(double x, double y) { + float[] pt = { + (float) x, (float) y + }; + mInverse.transform(pt); + return new Point(pt[0], pt[1]); + } + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event arg0) { + synchronized (TreeViewOverview.this) { + setTransform(); + } + doRedraw(); + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (TreeViewOverview.this) { + if (mTree != null) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + e.gc.setTransform(mTransform); + e.gc.setLineWidth((int) Math.ceil(0.7 / mScale)); + Path connectionPath = new Path(Display.getDefault()); + paintRecursive(e.gc, mTree, connectionPath); + e.gc.drawPath(connectionPath); + connectionPath.dispose(); + + if (mViewport != null) { + e.gc.setAlpha(50); + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math + .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); + + e.gc.setAlpha(255); + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_DARK_GRAY)); + e.gc.setLineWidth((int) Math.ceil(2 / mScale)); + e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math + .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); + } + } + } + } + }; + + private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) { + if (mSelectedNode == node && node.viewNode.filtered) { + gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); + } else if (mSelectedNode == node) { + gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); + } else if (node.viewNode.filtered) { + gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); + } else { + gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); + } + int N = node.children.size(); + if (N == 0) { + return; + } + float childSpacing = + (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N; + for (int i = 0; i < N; i++) { + DrawableViewNode child = node.children.get(i); + paintRecursive(gc, child, connectionPath); + float x1 = node.left + DrawableViewNode.NODE_WIDTH; + float y1 = + (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2; + float x2 = child.left; + float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; + float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy1 = y1; + float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy2 = y2; + connectionPath.moveTo(x1, y1); + connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); + } + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + public void loadAllData() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + doRedraw(); + } + + private void setBounds() { + if (mViewport != null && mTree != null) { + mBounds.x = Math.min(mViewport.x, mTree.bounds.x); + mBounds.y = Math.min(mViewport.y, mTree.bounds.y); + mBounds.width = + Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width) + - mBounds.x; + mBounds.height = + Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height) + - mBounds.y; + } else if (mTree != null) { + mBounds.x = mTree.bounds.x; + mBounds.y = mTree.bounds.y; + mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x; + mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y; + } + } + + private void setTransform() { + if (mTree != null) { + + mTransform.identity(); + mInverse.identity(); + final Point size = new Point(); + size.x = getBounds().width; + size.y = getBounds().height; + if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) { + mScale = 1; + } else { + mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height); + } + mTransform.scale((float) mScale, (float) mScale); + mInverse.scale((float) mScale, (float) mScale); + mTransform.translate((float) -mBounds.x, (float) -mBounds.y); + mInverse.translate((float) -mBounds.x, (float) -mBounds.y); + if (size.x / mBounds.width < size.y / mBounds.height) { + mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2); + mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2); + } else { + mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0); + mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0); + } + mInverse.invert(); + } + } + + @Override + public void viewportChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + viewportChanged(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java new file mode 100644 index 00000000..3343fdf8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui.util; + +import com.android.hierarchyviewerlib.models.ViewNode; + +import java.util.ArrayList; + +public class DrawableViewNode { + public ViewNode viewNode; + + public final ArrayList children = new ArrayList(); + + public final static int NODE_HEIGHT = 100; + + public final static int NODE_WIDTH = 180; + + public final static int CONTENT_LEFT_RIGHT_PADDING = 9; + + public final static int CONTENT_TOP_BOTTOM_PADDING = 8; + + public final static int CONTENT_INTER_PADDING = 3; + + public final static int INDEX_PADDING = 7; + + public final static int LEAF_NODE_SPACING = 9; + + public final static int NON_LEAF_NODE_SPACING = 15; + + public final static int PARENT_CHILD_SPACING = 50; + + public final static int PADDING = 30; + + public int treeHeight; + + public int treeWidth; + + public boolean leaf; + + public DrawableViewNode parent; + + public int left; + + public double top; + + public int topSpacing; + + public int bottomSpacing; + + public boolean treeDrawn; + + public static class Rectangle { + public double x, y, width, height; + + public Rectangle() { + + } + + public Rectangle(Rectangle other) { + this.x = other.x; + this.y = other.y; + this.width = other.width; + this.height = other.height; + } + + public Rectangle(double x, double y, double width, double height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public String toString() { + return "{" + x + ", " + y + ", " + width + ", " + height + "}"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ + } + + } + + public static class Point { + public double x, y; + + public Point() { + } + + public Point(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "(" + x + ", " + y + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + public Rectangle bounds = new Rectangle(); + + public DrawableViewNode(ViewNode viewNode) { + this.viewNode = viewNode; + treeDrawn = !viewNode.willNotDraw; + if (viewNode.children.size() == 0) { + treeHeight = NODE_HEIGHT; + treeWidth = NODE_WIDTH; + leaf = true; + } else { + leaf = false; + int N = viewNode.children.size(); + treeHeight = 0; + treeWidth = 0; + for (int i = 0; i < N; i++) { + DrawableViewNode child = new DrawableViewNode(viewNode.children.get(i)); + children.add(child); + child.parent = this; + treeHeight += child.treeHeight; + treeWidth = Math.max(treeWidth, child.treeWidth); + if (i != 0) { + DrawableViewNode prevChild = children.get(i - 1); + if (prevChild.leaf && child.leaf) { + treeHeight += LEAF_NODE_SPACING; + prevChild.bottomSpacing = LEAF_NODE_SPACING; + child.topSpacing = LEAF_NODE_SPACING; + } else { + treeHeight += NON_LEAF_NODE_SPACING; + prevChild.bottomSpacing = NON_LEAF_NODE_SPACING; + child.topSpacing = NON_LEAF_NODE_SPACING; + } + } + treeDrawn |= child.treeDrawn; + } + treeWidth += NODE_WIDTH + PARENT_CHILD_SPACING; + } + } + + public void setLeft() { + if (parent == null) { + left = PADDING; + bounds.x = 0; + bounds.width = treeWidth + 2 * PADDING; + } else { + left = parent.left + NODE_WIDTH + PARENT_CHILD_SPACING; + } + int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).setLeft(); + } + } + + public void placeRoot() { + top = PADDING + (treeHeight - NODE_HEIGHT) / 2.0; + double currentTop = PADDING; + int N = children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode child = children.get(i); + child.place(currentTop, top - currentTop); + currentTop += child.treeHeight + child.bottomSpacing; + } + bounds.y = 0; + bounds.height = treeHeight + 2 * PADDING; + } + + private void place(double treeTop, double rootDistance) { + if (treeHeight <= rootDistance) { + top = treeTop + treeHeight - NODE_HEIGHT; + } else if (rootDistance <= -NODE_HEIGHT) { + top = treeTop; + } else { + if (children.size() == 0) { + top = treeTop; + } else { + top = + rootDistance + treeTop - NODE_HEIGHT + (2.0 * NODE_HEIGHT) + / (treeHeight + NODE_HEIGHT) * (treeHeight - rootDistance); + } + } + int N = children.size(); + double currentTop = treeTop; + for (int i = 0; i < N; i++) { + DrawableViewNode child = children.get(i); + child.place(currentTop, rootDistance); + currentTop += child.treeHeight + child.bottomSpacing; + rootDistance -= child.treeHeight + child.bottomSpacing; + } + } + + public DrawableViewNode getSelected(double x, double y) { + if (x >= left && x < left + NODE_WIDTH && y >= top && y <= top + NODE_HEIGHT) { + return this; + } + int N = children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode selected = children.get(i).getSelected(x, y); + if (selected != null) { + return selected; + } + } + return null; + } + + /* + * Moves the node the specified distance up. + */ + public void move(double distance) { + top -= distance; + + // Get the root + DrawableViewNode root = this; + while (root.parent != null) { + root = root.parent; + } + + // Figure out the new tree top. + double treeTop; + if (top + NODE_HEIGHT <= root.top) { + treeTop = top + NODE_HEIGHT - treeHeight; + } else if (top >= root.top + NODE_HEIGHT) { + treeTop = top; + } else { + if (leaf) { + treeTop = top; + } else { + double distanceRatio = 1 - (root.top + NODE_HEIGHT - top) / (2.0 * NODE_HEIGHT); + treeTop = root.top - treeHeight + distanceRatio * (treeHeight + NODE_HEIGHT); + } + } + // Go up the tree and figure out the tree top. + DrawableViewNode node = this; + while (node.parent != null) { + int index = node.viewNode.index; + for (int i = 0; i < index; i++) { + DrawableViewNode sibling = node.parent.children.get(i); + treeTop -= sibling.treeHeight + sibling.bottomSpacing; + } + node = node.parent; + } + + // Update the bounds. + root.bounds.y = Math.min(root.top - PADDING, treeTop - PADDING); + root.bounds.height = + Math.max(treeTop + root.treeHeight + PADDING, root.top + NODE_HEIGHT + PADDING) + - root.bounds.y; + // Place all the children of the root + double currentTop = treeTop; + int N = root.children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode child = root.children.get(i); + child.place(currentTop, root.top - currentTop); + currentTop += child.treeHeight + child.bottomSpacing; + } + + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java new file mode 100644 index 00000000..3d4ac04b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui.util; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes PSD file. Supports only 8 bits, RGB images with 4 channels. + */ +public class PsdFile { + private final Header mHeader; + + private final ColorMode mColorMode; + + private final ImageResources mImageResources; + + private final LayersMasksInfo mLayersMasksInfo; + + private final LayersInfo mLayersInfo; + + private final BufferedImage mMergedImage; + + private final Graphics2D mGraphics; + + public PsdFile(int width, int height) { + mHeader = new Header(width, height); + mColorMode = new ColorMode(); + mImageResources = new ImageResources(); + mLayersMasksInfo = new LayersMasksInfo(); + mLayersInfo = new LayersInfo(); + + mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + mGraphics = mMergedImage.createGraphics(); + } + + public void addLayer(String name, BufferedImage image, Point offset) { + addLayer(name, image, offset, true); + } + + public void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayersInfo.addLayer(name, image, offset, visible); + if (visible) + mGraphics.drawImage(image, null, offset.x, offset.y); + } + + public void write(OutputStream stream) { + mLayersMasksInfo.setLayersInfo(mLayersInfo); + + DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream)); + try { + mHeader.write(out); + out.flush(); + + mColorMode.write(out); + mImageResources.write(out); + mLayersMasksInfo.write(out); + mLayersInfo.write(out); + out.flush(); + + mLayersInfo.writeImageData(out); + out.flush(); + + writeImage(mMergedImage, out, false); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static void writeImage(BufferedImage image, DataOutputStream out, boolean split) + throws IOException { + + if (!split) + out.writeShort(0); + + int width = image.getWidth(); + int height = image.getHeight(); + + final int length = width * height; + int[] pixels = new int[length]; + + image.getData().getDataElements(0, 0, width, height, pixels); + + byte[] a = new byte[length]; + byte[] r = new byte[length]; + byte[] g = new byte[length]; + byte[] b = new byte[length]; + + for (int i = 0; i < length; i++) { + final int pixel = pixels[i]; + a[i] = (byte) ((pixel >> 24) & 0xFF); + r[i] = (byte) ((pixel >> 16) & 0xFF); + g[i] = (byte) ((pixel >> 8) & 0xFF); + b[i] = (byte) (pixel & 0xFF); + } + + if (split) + out.writeShort(0); + if (split) + out.write(a); + if (split) + out.writeShort(0); + out.write(r); + if (split) + out.writeShort(0); + out.write(g); + if (split) + out.writeShort(0); + out.write(b); + if (!split) + out.write(a); + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Header { + static final short MODE_BITMAP = 0; + + static final short MODE_GRAYSCALE = 1; + + static final short MODE_INDEXED = 2; + + static final short MODE_RGB = 3; + + static final short MODE_CMYK = 4; + + static final short MODE_MULTI_CHANNEL = 7; + + static final short MODE_DUOTONE = 8; + + static final short MODE_LAB = 9; + + final byte[] mSignature = "8BPS".getBytes(); //$NON-NLS-1$ + + final short mVersion = 1; + + final byte[] mReserved = new byte[6]; + + final short mChannelCount = 4; + + final int mHeight; + + final int mWidth; + + final short mDepth = 8; + + final short mMode = MODE_RGB; + + Header(int width, int height) { + mWidth = width; + mHeight = height; + } + + void write(DataOutputStream out) throws IOException { + out.write(mSignature); + out.writeShort(mVersion); + out.write(mReserved); + out.writeShort(mChannelCount); + out.writeInt(mHeight); + out.writeInt(mWidth); + out.writeShort(mDepth); + out.writeShort(mMode); + } + } + + // Unused at the moment + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class ColorMode { + final int mLength = 0; + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + } + } + + // Unused at the moment + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class ImageResources { + static final short RESOURCE_RESOLUTION_INFO = 0x03ED; + + int mLength = 0; + + final byte[] mSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final short mResourceId = RESOURCE_RESOLUTION_INFO; + + final short mPad = 0; + + final int mDataLength = 16; + + final short mHorizontalDisplayUnit = 0x48; // 72 dpi + + final int mHorizontalResolution = 1; + + final short mWidthDisplayUnit = 1; + + final short mVerticalDisplayUnit = 0x48; // 72 dpi + + final int mVerticalResolution = 1; + + final short mHeightDisplayUnit = 1; + + ImageResources() { + mLength = mSignature.length; + mLength += 2; + mLength += 2; + mLength += 4; + mLength += 8; + mLength += 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + out.write(mSignature); + out.writeShort(mResourceId); + out.writeShort(mPad); + out.writeInt(mDataLength); + out.writeShort(mHorizontalDisplayUnit); + out.writeInt(mHorizontalResolution); + out.writeShort(mWidthDisplayUnit); + out.writeShort(mVerticalDisplayUnit); + out.writeInt(mVerticalResolution); + out.writeShort(mHeightDisplayUnit); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class LayersMasksInfo { + int mMiscLength; + + int mLayerInfoLength; + + void setLayersInfo(LayersInfo layersInfo) { + mLayerInfoLength = layersInfo.getLength(); + // Round to the next multiple of 2 + if ((mLayerInfoLength & 0x1) == 0x1) + mLayerInfoLength++; + mMiscLength = mLayerInfoLength + 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mMiscLength); + out.writeInt(mLayerInfoLength); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class LayersInfo { + final List mLayers = new ArrayList(); + + void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayers.add(new Layer(name, image, offset, visible)); + } + + int getLength() { + int length = 2; + for (Layer layer : mLayers) { + length += layer.getLength(); + } + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort((short) -mLayers.size()); + for (Layer layer : mLayers) { + layer.write(out); + } + } + + void writeImageData(DataOutputStream out) throws IOException { + for (Layer layer : mLayers) { + layer.writeImageData(out); + } + // Global layer mask info length + out.writeInt(0); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Layer { + static final byte OPACITY_TRANSPARENT = 0x0; + + static final byte OPACITY_OPAQUE = (byte) 0xFF; + + static final byte CLIPPING_BASE = 0x0; + + static final byte CLIPPING_NON_BASE = 0x1; + + static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1; + + static final byte FLAG_INVISIBLE = 0x2; + + final int mTop; + + final int mLeft; + + final int mBottom; + + final int mRight; + + final short mChannelCount = 4; + + final Channel[] mChannelInfo = new Channel[mChannelCount]; + + final byte[] mBlendSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final byte[] mBlendMode = "norm".getBytes(); //$NON-NLS-1$ + + final byte mOpacity = OPACITY_OPAQUE; + + final byte mClipping = CLIPPING_BASE; + + byte mFlags = 0x0; + + final byte mFiller = 0x0; + + int mExtraSize = 4 + 4; + + final int mMaskDataLength = 0; + + final int mBlendRangeDataLength = 0; + + final byte[] mName; + + final byte[] mLayerExtraSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final byte[] mLayerExtraKey = "luni".getBytes(); //$NON-NLS-1$ + + int mLayerExtraLength; + + final String mOriginalName; + + private BufferedImage mImage; + + Layer(String name, BufferedImage image, Point offset, boolean visible) { + final int height = image.getHeight(); + final int width = image.getWidth(); + final int length = width * height; + + mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length); + mChannelInfo[1] = new Channel(Channel.ID_RED, length); + mChannelInfo[2] = new Channel(Channel.ID_GREEN, length); + mChannelInfo[3] = new Channel(Channel.ID_BLUE, length); + + mTop = offset.y; + mLeft = offset.x; + mBottom = offset.y + height; + mRight = offset.x + width; + + mOriginalName = name; + byte[] data = name.getBytes(); + + try { + mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; //$NON-NLS-1$ + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + final byte[] nameData = new byte[data.length + 1]; + nameData[0] = (byte) (data.length & 0xFF); + System.arraycopy(data, 0, nameData, 1, data.length); + + // This could be done in the same pass as above + if (nameData.length % 4 != 0) { + data = new byte[nameData.length + 4 - (nameData.length % 4)]; + System.arraycopy(nameData, 0, data, 0, nameData.length); + mName = data; + } else { + mName = nameData; + } + mExtraSize += mName.length; + mExtraSize += + mLayerExtraLength + 4 + mLayerExtraKey.length + mLayerExtraSignature.length; + + mImage = image; + + if (!visible) { + mFlags |= FLAG_INVISIBLE; + } + } + + int getLength() { + int length = 4 * 4 + 2; + + for (Channel channel : mChannelInfo) { + length += channel.getLength(); + } + + length += mBlendSignature.length; + length += mBlendMode.length; + length += 4; + length += 4; + length += mExtraSize; + + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mTop); + out.writeInt(mLeft); + out.writeInt(mBottom); + out.writeInt(mRight); + + out.writeShort(mChannelCount); + for (Channel channel : mChannelInfo) { + channel.write(out); + } + + out.write(mBlendSignature); + out.write(mBlendMode); + + out.write(mOpacity); + out.write(mClipping); + out.write(mFlags); + out.write(mFiller); + + out.writeInt(mExtraSize); + out.writeInt(mMaskDataLength); + + out.writeInt(mBlendRangeDataLength); + + out.write(mName); + + out.write(mLayerExtraSignature); + out.write(mLayerExtraKey); + out.writeInt(mLayerExtraLength); + out.writeInt(mOriginalName.length() + 1); + out.write(mOriginalName.getBytes("UTF-16")); //$NON-NLS-1$ + } + + void writeImageData(DataOutputStream out) throws IOException { + writeImage(mImage, out, true); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Channel { + static final short ID_RED = 0; + + static final short ID_GREEN = 1; + + static final short ID_BLUE = 2; + + static final short ID_ALPHA = -1; + + static final short ID_LAYER_MASK = -2; + + final short mId; + + final int mDataLength; + + Channel(short id, int dataLength) { + mId = id; + mDataLength = dataLength + 2; + } + + int getLength() { + return 2 + 4 + mDataLength; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort(mId); + out.writeInt(mDataLength); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java new file mode 100644 index 00000000..f1ea61a6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui.util; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.TreeColumn; + +public class TreeColumnResizer { + + private TreeColumn mColumn1; + + private TreeColumn mColumn2; + + private Composite mControl; + + private int mColumn1Width; + + private int mColumn2Width; + + private final static int MIN_COLUMN1_WIDTH = 18; + + private final static int MIN_COLUMN2_WIDTH = 3; + + public TreeColumnResizer(Composite control, TreeColumn column1, TreeColumn column2) { + this.mControl = control; + this.mColumn1 = column1; + this.mColumn2 = column2; + control.addListener(SWT.Resize, resizeListener); + column1.addListener(SWT.Resize, column1ResizeListener); + column2.setResizable(false); + } + + private Listener resizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + if (mColumn1Width == 0 && mColumn2Width == 0) { + mColumn1Width = (mControl.getBounds().width - 18) / 2; + mColumn2Width = (mControl.getBounds().width - 18) / 2; + } else { + int dif = mControl.getBounds().width - 18 - (mColumn1Width + mColumn2Width); + int columnDif = Math.abs(mColumn1Width - mColumn2Width); + int mainColumnChange = Math.min(Math.abs(dif), columnDif); + int left = Math.max(0, Math.abs(dif) - columnDif); + if (dif < 0) { + if (mColumn1Width > mColumn2Width) { + mColumn1Width -= mainColumnChange; + } else { + mColumn2Width -= mainColumnChange; + } + mColumn1Width -= left / 2; + mColumn2Width -= left - left / 2; + } else { + if (mColumn1Width > mColumn2Width) { + mColumn2Width += mainColumnChange; + } else { + mColumn1Width += mainColumnChange; + } + mColumn1Width += left / 2; + mColumn2Width += left - left / 2; + } + } + mColumn1.removeListener(SWT.Resize, column1ResizeListener); + mColumn1.setWidth(mColumn1Width); + mColumn2.setWidth(mColumn2Width); + mColumn1.addListener(SWT.Resize, column1ResizeListener); + } + }; + + private Listener column1ResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + int widthDif = mColumn1Width - mColumn1.getWidth(); + mColumn1Width -= widthDif; + mColumn2Width += widthDif; + boolean column1Changed = false; + + // Strange, but these constants make the columns look the same. + + if (mColumn1Width < MIN_COLUMN1_WIDTH) { + mColumn2Width -= MIN_COLUMN1_WIDTH - mColumn1Width; + mColumn1Width += MIN_COLUMN1_WIDTH - mColumn1Width; + column1Changed = true; + } + if (mColumn2Width < MIN_COLUMN2_WIDTH) { + mColumn1Width += mColumn2Width - MIN_COLUMN2_WIDTH; + mColumn2Width = MIN_COLUMN2_WIDTH; + column1Changed = true; + } + if (column1Changed) { + mColumn1.removeListener(SWT.Resize, this); + mColumn1.setWidth(mColumn1Width); + mColumn1.addListener(SWT.Resize, this); + } + mColumn2.setWidth(mColumn2Width); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java new file mode 100644 index 00000000..727daf5c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.models; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Display; + +import junit.framework.TestCase; + +public class EvaluateContrastModelTest extends TestCase { + + private static ImageLoader sImageLoader; + private static final int ARGB_BLACK = 0xFF000000; + private static final int ARGB_WHITE = 0XFFFFFFFF; + private static final int ARGB_DARK_GREEN = 0xFF0D6F57; + private static final int ARGB_LIGHT_GREEN = 0xFF1DE9B6; + + @Override + protected void setUp() throws Exception { + super.setUp(); + sImageLoader = ImageLoader.getLoader(EvaluateContrastModelTest.class); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + sImageLoader.dispose(); + } + + private static int argbToRgb(int argb) { + return 0xFFFFFF & argb; + } + + public void testFailsAllCases() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + + // Text color is irrelevant in these tests because it will be calculated. + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + assertFalse(model.isIndeterminate()); + + // text size is normal, bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, + bounds.height, true); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size is normal for bold, bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size is large, not bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + } + + public void testPassesAllCases() { + Image[] images = { + sImageLoader.loadImage("black_on_white.png", Display.getDefault()), + sImageLoader.loadImage("white_on_black.png", Display.getDefault()), + }; + int[] textColors = {ARGB_BLACK, ARGB_WHITE}; + Image image; + Rectangle bounds; + + // Text color is irrelevant in these tests because it will be calculated. + for (int i = 0; i < images.length; ++i) { + image = images[i]; + bounds = image.getBounds(); + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(image, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + assertFalse(model.isIndeterminate()); + + // text size is normal, bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size is normal for bold, bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size is large, not bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + } + } + + public void testIndeterminateFailsNormalAndPassesLarge() { + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + Rectangle bounds = greens.getBounds(); + + // Not providing a text size is the main cause for an indeterminate result. + // Text color is irrelevant because it will be calculated. + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); + assertTrue(model.isIndeterminate()); + + // no text size, bold + model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); + assertTrue(model.isIndeterminate()); + + // text size normal, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size normal, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size normal for bold, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size normal for bold, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size large, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size large, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + } + + public void testGetBackgroundColor() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); + + Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); + bounds = blackOnWhite.getBounds(); + model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_WHITE), model.getBackgroundColor()); + + Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); + bounds = whiteOnBlack.getBounds(); + model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); + + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + bounds = greens.getBounds(); + model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, + bounds.height, true); + assertEquals(argbToRgb(ARGB_LIGHT_GREEN), model.getBackgroundColor()); + } + + public void testGetTextColor() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); + model = new EvaluateContrastModel(allBlack, ARGB_BLACK, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ARGB_BLACK, model.getTextColor()); + + Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); + bounds = blackOnWhite.getBounds(); + model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); + + Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); + bounds = whiteOnBlack.getBounds(); + model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_WHITE), model.getTextColor()); + + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + bounds = greens.getBounds(); + model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(argbToRgb(ARGB_DARK_GREEN), model.getTextColor()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java new file mode 100644 index 00000000..67a632b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.hierarchyviewerlib.ui; + +import junit.framework.TestCase; + +public class PropertyViewerTest extends TestCase { + + public void testExistingCases() { + assertEquals("alpha", PropertyViewer.parseColumnTextName("drawing:getAlpha()")); + assertEquals("x", PropertyViewer.parseColumnTextName("drawing:getX()")); + } + + public void testEdgeCases() { + assertEquals("alpha", PropertyViewer.parseColumnTextName("foo:alpha")); + assertEquals("x", PropertyViewer.parseColumnTextName("foo:x")); + + assertEquals("get", PropertyViewer.parseColumnTextName("foo:get")); + assertEquals("", PropertyViewer.parseColumnTextName("foo:()")); + assertEquals("", PropertyViewer.parseColumnTextName("foo:")); + + assertEquals("getter", PropertyViewer.parseColumnTextName("foo:getter")); + assertEquals("together", PropertyViewer.parseColumnTextName("foo:together")); + assertEquals("together", PropertyViewer.parseColumnTextName("foo:getTogether")); + assertEquals("()get", PropertyViewer.parseColumnTextName("foo:()get")); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png new file mode 100644 index 00000000..56acc0c3 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png differ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png new file mode 100644 index 00000000..d3006471 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png differ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png new file mode 100644 index 00000000..7dc531fd Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png differ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png new file mode 100644 index 00000000..7dc11dfc Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.classpath b/andmore-swt/org.eclipse.andmore.sdkstats/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore b/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.project b/andmore-swt/org.eclipse.andmore.sdkstats/.project new file mode 100644 index 00000000..8fa5963c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.sdkstats + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF new file mode 100644 index 00000000..95b2a485 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.sdkstats;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt +Export-Package: com.android.sdkstats +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/NOTICE b/andmore-swt/org.eclipse.andmore.sdkstats/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/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/andmore-swt/org.eclipse.andmore.sdkstats/README b/andmore-swt/org.eclipse.andmore.sdkstats/README new file mode 100644 index 00000000..bda8ef84 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/README @@ -0,0 +1,11 @@ +How to use the Eclipse projects for SdkStats. + +SdkStats requires SWT to compile. + +SWT is available in the depot under //device/prebuild//swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at //device/prebuild//swt. diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle b/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle new file mode 100644 index 00000000..1f9261c2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle @@ -0,0 +1,15 @@ +group = 'com.android.tools' +archivesBaseName = 'sdkstats' + +dependencies { + compile project(':base:common') + compile project(':swt:swtmenubar') + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/build.properties b/andmore-swt/org.eclipse.andmore.sdkstats/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties b/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties new file mode 100644 index 00000000..a4ad3878 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.uiautomatorviewer +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI SDK Statistics +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml b/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml new file mode 100644 index 00000000..861f9c3b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.sdkstats + eclipse-plugin + sdkstats + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml b/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml new file mode 100644 index 00000000..cb715fd8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java new file mode 100644 index 00000000..7f12f0e1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkstats; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import org.eclipse.jface.preference.PreferenceStore; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Random; + +/** + * Manages persistence settings for DDMS. + * + * For convenience, this also stores persistence settings related to the "server stats" ping + * as well as some ADT settings that are SDK specific but not workspace specific. + */ +public class DdmsPreferenceStore { + + public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$ + private final static String PING_TIME = "pingTime"; //$NON-NLS-1$ + private final static String PING_ID = "pingId"; //$NON-NLS-1$ + + private final static String ADT_USED = "adtUsed"; //$NON-NLS-1$ + private final static String LAST_SDK_PATH = "lastSdkPath"; //$NON-NLS-1$ + + /** + * PreferenceStore for DDMS. + * Creation and usage must be synchronized on {@code DdmsPreferenceStore.class}. + * Don't use it directly, instead retrieve it via {@link #getPreferenceStore()}. + */ + private static volatile PreferenceStore sPrefStore; + + public DdmsPreferenceStore() { + } + + /** + * Returns the DDMS {@link PreferenceStore}. + * This keeps a static reference on the store, so consequent calls will + * return always the same store. + */ + public PreferenceStore getPreferenceStore() { + synchronized (DdmsPreferenceStore.class) { + if (sPrefStore == null) { + // get the location of the preferences + String homeDir = null; + try { + homeDir = AndroidLocation.getFolder(); + } catch (AndroidLocationException e1) { + // pass, we'll do a dummy store since homeDir is null + } + + if (homeDir == null) { + sPrefStore = new PreferenceStore(); + return sPrefStore; + } + + assert homeDir != null; + + String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$ + + // also look for an old pref file in the previous location + String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$ + + File.separator + ".ddmsrc"; //$NON-NLS-1$ + File oldPrefFile = new File(oldPrefPath); + if (oldPrefFile.isFile()) { + FileOutputStream fileOutputStream = null; + try { + PreferenceStore oldStore = new PreferenceStore(oldPrefPath); + oldStore.load(); + + fileOutputStream = new FileOutputStream(rcFileName); + oldStore.save(fileOutputStream, ""); //$NON-NLS-1$ + oldPrefFile.delete(); + + PreferenceStore newStore = new PreferenceStore(rcFileName); + newStore.load(); + sPrefStore = newStore; + } catch (IOException e) { + // create a new empty store. + sPrefStore = new PreferenceStore(rcFileName); + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException e) { + // pass + } + } + } + } else { + sPrefStore = new PreferenceStore(rcFileName); + + try { + sPrefStore.load(); + } catch (IOException e) { + System.err.println("Error Loading DDMS Preferences"); + } + } + } + + assert sPrefStore != null; + return sPrefStore; + } + } + + /** + * Save the prefs to the config file. + */ + public void save() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + try { + prefs.save(); + } + catch (IOException ioe) { + // FIXME com.android.dmmlib.Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); + } + } + } + + // ---- Utility methods to access some specific prefs ---- + + /** + * Indicates whether the ping ID is set. + * This should be true when {@link #isPingOptIn()} is true. + * + * @return true if a ping ID is set, which means the user gave permission + * to use the ping service. + */ + public boolean hasPingId() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + return prefs != null && prefs.contains(PING_ID); + } + } + + /** + * Retrieves the current ping ID, if set. + * To know if the ping ID is set, use {@link #hasPingId()}. + *

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

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

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

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

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

+ * The arguments are defined as follow: + *

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

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

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

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

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

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

+     * JVM:       Java 32-bit   Java 64-bit
+     * Windows:   x86           x86_64
+     * Linux:     x86           x86_64
+     * Mac        untested      x86_64
+     * 
+ */ + protected String getJvmArch() { // made protected for testing + String arch = getSystemProperty(SYS_PROP_OS_ARCH); + return sanitizeOsArch(arch); + } + + private String sanitizeOsArch(String arch) { + if (arch == null || arch.length() == 0) { + return "unknown"; //$NON-NLS-1$ + } + + if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ + return "x86_64"; //$NON-NLS-1$ + } + + if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$ + // Any variation of iX86 counts as x86 (i386, i486, i686). + return "x86"; //$NON-NLS-1$ + } + + if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$ + return "ppc"; //$NON-NLS-1$ + } + + // Unknown arch. Send it as-is but protect against arbitrarily long values. + if (arch.length() > 32) { + arch = arch.substring(0, 32); + } + return arch; + } + + /** + * Normalize the supplied application name. + * + * @param app to report + */ + protected String normalizeAppName(String app) { + // Filter out \W , non-word character: [^a-zA-Z_0-9] + String app2 = app.replaceAll("\\W", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + if (app.length() == 0) { + throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$ + } + + return app2; + } + + /** + * Validate the supplied application version, and normalize the version. + * + * @param version supplied by caller + * @return normalized dotted quad version + */ + protected String normalizeVersion(String version) { + + Pattern regex = Pattern.compile( + //1=major 2=minor 3=micro 4=build | 5=rc + "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$ + + Matcher m = regex.matcher(version); + if (m != null && m.lookingAt()) { + StringBuilder normal = new StringBuilder(); + for (int i = 1; i <= 4; i++) { + int v = 0; + // If build is null but we have an rc, take that number instead as the 4th part. + if (i == 4 && + i < m.groupCount() && + m.group(i) == null && + m.group(i+1) != null) { + i++; + } + if (m.group(i) != null) { + try { + v = Integer.parseInt(m.group(i)); + } catch (Exception ignore) { + } + } + if (i > 1) { + normal.append('.'); + } + normal.append(v); + } + return normal.toString(); + } + + throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$ + } + + /** + * Calls {@link System#getProperty(String)}. + * Allows unit-test to override the return value. + * @see System#getProperty(String) + */ + protected String getSystemProperty(String name) { + return System.getProperty(name); + } + + /** + * Calls {@link System#getenv(String)}. + * Allows unit-test to override the return value. + * @see System#getenv(String) + */ + protected String getSystemEnv(String name) { + return System.getenv(name); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java new file mode 100644 index 00000000..69402b6e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkstats; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class SdkStatsServiceTest extends TestCase { + + private static class MockSdkStatsService extends SdkStatsService { + + private final String mOsName; + private final String mOsVersion; + private final String mOsArch; + private final String mJavaVersion; + private final Map mEnvVars = new HashMap(); + private URL mPingUrlResult; + + public MockSdkStatsService(String osName, + String osVersion, + String osArch, + String javaVersion) { + mOsName = osName; + mOsVersion = osVersion; + mOsArch = osArch; + mJavaVersion = javaVersion; + } + + public URL getPingUrlResult() { + return mPingUrlResult; + } + + public void setSystemEnv(String varName, String value) { + mEnvVars.put(varName, value); + } + + @Override + protected String getSystemProperty(String name) { + if (SdkStatsService.SYS_PROP_OS_NAME.equals(name)) { + return mOsName; + } else if (SdkStatsService.SYS_PROP_OS_VERSION.equals(name)) { + return mOsVersion; + } else if (SdkStatsService.SYS_PROP_OS_ARCH.equals(name)) { + return mOsArch; + } else if (SdkStatsService.SYS_PROP_JAVA_VERSION.equals(name)) { + return mJavaVersion; + } + // Don't use current properties values, we don't want the tests to be flaky + fail("SdkStatsServiceTest doesn't define a system.property for " + name); + return null; + } + + @Override + protected String getSystemEnv(String name) { + if (mEnvVars.containsKey(name)) { + return mEnvVars.get(name); + } + // Don't use current env vars, we don't want the tests to be flaky + fail("SdkStatsServiceTest doesn't define a system.getenv for " + name); + return null; + } + + @Override + protected void doPing(String app, String version, + Map extras) { + // The super.doPing() does: + // 1- normalize input, + // 2- check the ping time, + // 3- check/create the pind id, + // 4- create the ping URL + // 5- and send the network ping in a thread. + // In this mock version we just do steps 1 and 4 and record the URL; + // obvious we don't check the ping time in the prefs nor send the actual ping. + + // Validate the application and version input. + final String nApp = normalizeAppName(app); + final String nVersion = normalizeVersion(version); + + long id = 0x42; + try { + mPingUrlResult = createPingUrl(nApp, nVersion, id, extras); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testSdkStatsService_getJvmArch() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i386", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i486", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Linux", "4.0", "i486-linux", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i586", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i686", "1.7"); + assertEquals("x86", m.getJvmArch()); + + m = new MockSdkStatsService("Mac OS", "10.0", "x86_64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Mac OS", "8.0", "PowerPC", "1.7"); + assertEquals("ppc", m.getJvmArch()); + + m = new MockSdkStatsService("Mac OS", "4.0", "x86_64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "ia64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", "atom", "1.7"); + assertEquals("atom", m.getJvmArch()); + + // 32 chars max + m = new MockSdkStatsService("Windows", "4.0", + "one3456789ten3456789twenty6789thirty6789", "1.7"); + assertEquals("one3456789ten3456789twenty6789th", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", "", "1.7"); + assertEquals("unknown", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", null, "1.7"); + assertEquals("unknown", m.getJvmArch()); + } + + public void testSdkStatsService_getJvmVersion() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("1.7", m.getJvmVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", ""); + assertEquals("unknown", m.getJvmVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", null); + assertEquals("unknown", m.getJvmVersion()); + + // 8 chars max + m = new MockSdkStatsService("Windows", "4.0", "x86", + "one3456789ten3456789twenty6789thirty6789"); + assertEquals("one34567", m.getJvmVersion()); + } + + public void testSdkStatsService_getJvmInfo() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("1.7-x86", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7.8_09"); + assertEquals("1.7-x86_64", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", "", ""); + assertEquals("unknown-unknown", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", null, null); + assertEquals("unknown-unknown", m.getJvmInfo()); + + // 8+32 chars max + m = new MockSdkStatsService("Windows", "4.0", + "one3456789ten3456789twenty6789thirty6789", + "one3456789ten3456789twenty6789thirty6789"); + assertEquals("one34567-one3456789ten3456789twenty6789th", m.getJvmInfo()); + } + + public void testSdkStatsService_getOsVersion() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0.32", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4", "x86", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4.0;extrainfo", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("10.8", m.getOsVersion()); + + m = new MockSdkStatsService("Mac OS", "10.8", "x86_64", "1.7.8_09"); + assertEquals("10.8", m.getOsVersion()); + + m = new MockSdkStatsService("Other", "", "x86_64", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + + m = new MockSdkStatsService("Other", null, "x86_64", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + } + + public void testSdkStatsService_getOsArch() { + MockSdkStatsService m; + + // 64 bit jvm + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + // 32 bit jvm with 32 vs 64 bit os + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", null); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", "AMD64"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", "IA64"); + assertEquals("x86_64", m.getOsArch()); + + // 32 bit jvm with 32 vs 64 bit os + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", null); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "i686-linux"); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "AMD64"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "x86_64"); + assertEquals("x86_64", m.getOsArch()); + } + + public void testSdkStatsService_getOsName() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("mac-10.8", m.getOsName()); + + m = new MockSdkStatsService("mac", "10", "x86", "1.7.8_09"); + assertEquals("mac", m.getOsName()); + + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + assertEquals("win-6.2", m.getOsName()); + + m = new MockSdkStatsService("win", "6.2", "x86", "1.7.8_09"); + assertEquals("win-6.2", m.getOsName()); + + m = new MockSdkStatsService("win", "6", "x86_64", "1.7.8_09"); + assertEquals("win", m.getOsName()); + + m = new MockSdkStatsService("Linux", "foobuntu-32", "x86", "1.7.8_09"); + assertEquals("linux", m.getOsName()); + + m = new MockSdkStatsService("linux", "1", "x86_64", "1.7.8_09"); + assertEquals("linux", m.getOsName()); + + m = new MockSdkStatsService("PowerPC", "32", "ppc", "1.7.8_09"); + assertEquals("PowerPC", m.getOsName()); + + m = new MockSdkStatsService("freebsd", "42", "x86_64", "1.7.8_09"); + assertEquals("freebsd", m.getOsName()); + + m = new MockSdkStatsService("openbsd", "43", "x86_64", "1.7.8_09"); + assertEquals("openbsd", m.getOsName()); + + // 32 chars max + m = new MockSdkStatsService("one3456789ten3456789twenty6789thirty6789", + "42", "x86_64", "1.7.8_09"); + assertEquals("one3456789ten3456789twenty6789th", m.getOsName()); + } + + public void testSdkStatsService_parseVersion() { + // Tests that the version parses supports the new "major.minor.micro rcPreview" format + // as well as "x.y.z.t" formats as well as Eclipse's "x.y.z.v2012somedate" formats. + + MockSdkStatsService m; + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + + m.ping("monitor", "21"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.1"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.1.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.03"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.3.4"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // More than 4 parts or extra stuff that is not an "rc" preview are ignored. + m.ping("monitor", "21.2.3.4.5.6.7.8"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.3.4.v20120101 the rest is ignored"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // If the "rc" preview integer is present, it's equivalent to a 4th number. + m.ping("monitor", "21 rc4"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.0.0.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.01 rc5"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.1.0.5&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.02.03 rc6"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.6&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // If there's a 4-part version number, the rc preview number isn't used. + m.ping("monitor", "21.2.3.4 rc7"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // For Eclipse plugins, the 4th part might be a date. It is ignored. + m.ping("eclipse", "21.2.3.v20120102235958"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_eclipse&" + + "id=42&" + + "version=21.2.3.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + } + + public void testSdkStatsService_glPing() { + MockSdkStatsService m; + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + + // Send emulator ping with just emulator version, no GL stuff + m.ping("emulator", "12"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send emulator ping with just emulator version, no GL stuff. + // This is the same request but using the variable string list API, arg 0 is the "ping" app. + m.ping(new String[] { "ping", "emulator", "12" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send a ping for a non-emulator app with extra parameters, no GL stuff + m.ping(new String[] { "ping", "not-emulator", "12", "arg1", "arg2", "arg3" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_notemulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 3 parameters + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)", "1.2.3.4_preview" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.&" + + "glr=Some+cool_GPU+%28fast+one+%29&" + + "glv=1.2.3.4_preview", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 2 parameters + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.&" + + "glr=Some+cool_GPU+%28fast+one+%29", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 1 parameter + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc." }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.", + m.getPingUrlResult().toString()); + + // Parameters that are more than 128 chars are cut short. + m.ping(new String[] { "ping", "emulator", "12", + // 130 chars each + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + + "glr=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + + "glv=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567", + m.getPingUrlResult().toString()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath b/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath new file mode 100644 index 00000000..bb29c4da --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore b/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.project b/andmore-swt/org.eclipse.andmore.sdkuilib/.project new file mode 100644 index 00000000..c471b99d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.sdkuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..662c7dfc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 +encoding/test-src=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..529ef073 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +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.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..e1c0c58a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF @@ -0,0 +1,27 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.sdkuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.eclipse.andmore.swt, + org.eclipse.andmore.ddmuilib, + org.eclipse.andmore.swtmenubar, + com.google.gson;bundle-version="2.2.4", + org.eclipse.core.runtime;bundle-version="3.12.0", + org.eclipse.andmore.ddms;bundle-version="0.5.2" +Bundle-ClassPath: . +Export-Package: com.android.sdkuilib.repository, + com.android.sdkuilib.ui, + com.android.sdkuilib.widgets, + org.eclipse.andmore.sdktool +Import-Package: org.eclipse.ui.plugin +Bundle-ActivationPolicy: lazy +Bundle-Activator: org.eclipse.andmore.sdktool.SdkUserInterfacePlugin diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/MODULE_LICENSE_APACHE2 b/andmore-swt/org.eclipse.andmore.sdkuilib/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE b/andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/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/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties new file mode 100644 index 00000000..a5ed72ec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.properties,\ + icons/ + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/accept_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/accept_icon16.png new file mode 100644 index 00000000..ae61f7df Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/accept_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/addon_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/addon_pkg_16.png new file mode 100644 index 00000000..addef8ef Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/addon_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_128.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_128.png new file mode 100644 index 00000000..830c04b0 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_128.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_16.png new file mode 100644 index 00000000..08ffda85 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/archive_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/archive_icon16.png new file mode 100644 index 00000000..be5edd79 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/archive_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_16.png new file mode 100644 index 00000000..945d871b Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_pkg_16.png new file mode 100644 index 00000000..6daa67b8 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/buildtool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/buildtool_pkg_16.png new file mode 100644 index 00000000..9b917da4 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/buildtool_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/device.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/device.png new file mode 100644 index 00000000..7dbbbb6a Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/device.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/doc_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/doc_pkg_16.png new file mode 100644 index 00000000..a2be37af Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/doc_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/emulator.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/emulator.png new file mode 100644 index 00000000..a7180428 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/emulator.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/error_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/error_icon_16.png new file mode 100644 index 00000000..ccb4d0aa Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/error_icon_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/extra_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/extra_pkg_16.png new file mode 100644 index 00000000..7ad8a669 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/extra_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/incompat_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/incompat_icon16.png new file mode 100644 index 00000000..2a307e92 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/incompat_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_off_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_off_16.png new file mode 100644 index 00000000..ad2edff6 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_off_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_on_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_on_16.png new file mode 100644 index 00000000..ed27b520 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_on_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/nopkg_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/nopkg_icon_16.png new file mode 100644 index 00000000..147837fd Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/nopkg_icon_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_incompat_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_incompat_16.png new file mode 100644 index 00000000..d7d3ae6e Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_incompat_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_installed_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_installed_16.png new file mode 100644 index 00000000..70295655 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_installed_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_new_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_new_16.png new file mode 100644 index 00000000..9c93afc8 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_new_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_update_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_update_16.png new file mode 100644 index 00000000..4171ba63 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_update_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_16.png new file mode 100644 index 00000000..0ee32bf8 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_other_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_other_16.png new file mode 100644 index 00000000..395a2403 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_other_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platform_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platform_pkg_16.png new file mode 100644 index 00000000..56e10d0a Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platform_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platformtool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platformtool_pkg_16.png new file mode 100644 index 00000000..424fb295 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platformtool_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/reject_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/reject_icon16.png new file mode 100644 index 00000000..18c14811 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/reject_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sample_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sample_pkg_16.png new file mode 100644 index 00000000..11210bae Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sample_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sdkman_logo_128.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sdkman_logo_128.png new file mode 100644 index 00000000..0f1670d7 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sdkman_logo_128.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/source_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/source_pkg_16.png new file mode 100644 index 00000000..ab08d29a Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/source_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/status_ok_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/status_ok_16.png new file mode 100644 index 00000000..ae61f7df Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/status_ok_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_disabled_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_disabled_16.png new file mode 100644 index 00000000..721d1841 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_disabled_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_enabled_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_enabled_16.png new file mode 100644 index 00000000..198f2998 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_enabled_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sysimg_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sysimg_pkg_16.png new file mode 100644 index 00000000..942ce47a Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sysimg_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_16.png new file mode 100644 index 00000000..8887bcd3 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_32.png new file mode 100644 index 00000000..13fed144 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_32.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_16.png new file mode 100644 index 00000000..652c5efa Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_32.png new file mode 100644 index 00000000..33560163 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_32.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_16.png new file mode 100644 index 00000000..e3ad2e60 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_32.png new file mode 100644 index 00000000..f4264e7b Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_32.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tool_pkg_16.png new file mode 100644 index 00000000..424fb295 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tool_pkg_16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/unknown_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/unknown_icon16.png new file mode 100644 index 00000000..2b255fa9 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/unknown_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/icons/warning_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/warning_icon16.png new file mode 100644 index 00000000..a2fcf7cf Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/warning_icon16.png differ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.properties b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.properties new file mode 100644 index 00000000..8e651073 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.sdkuilib +Bundle-Vendor = Eclipse Andmore +Bundle-Name = Android SDK Library +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml b/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml new file mode 100644 index 00000000..803cf57a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + org.eclipse.andmore.sdkuilib + eclipse-plugin + sdkuilib + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + junit + junit + test + 4.12 + + + com.android.tools + dvlib + ${android.tools.version} + test + + + + + ${basedir}/test-src + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + test + test + + + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compiletests + test-compile + + testCompile + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java new file mode 100644 index 00000000..f32d6437 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java @@ -0,0 +1,119 @@ +/* + * 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.sdkuilib.internal.repository; + + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import com.android.SdkConstants; +import com.android.repository.api.LocalPackage; +import com.android.repository.io.FileOp; +import com.android.repository.io.FileOpUtils; +import com.android.sdklib.repository.PkgProps; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +public class AboutDialog extends UpdaterBaseDialog { + + private static final String COPYRIGHT = "Copyright (C) 2009-2017"; + + public AboutDialog(Shell parentShell, SdkContext sdkContext) { + super(parentShell, sdkContext, "About" /*title*/); + } + + @Override + protected void createContents() { + super.createContents(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(450, 150)); + shell.setSize(450, 150); + + GridLayoutBuilder.create(shell).columns(3); + + Label logo = new Label(shell, SWT.NONE); + ImageFactory imgf = mSdkContext.getSdkHelper().getImageFactory(); + Image image = imgf == null ? null : imgf.getImageByName("sdkman_logo_128.png"); + if (image != null) logo.setImage(image); + + Label label = new Label(shell, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + String version = getVersion(); + if (!version.isEmpty()) + version = String.format("Revision %1$s", version); + label.setText(String.format( + "Android SDK Manager.\n" + + "%1$s\n" + + "%2$s The Android Open Source Project.", + version, + COPYRIGHT)); + + Label filler = new Label(shell, SWT.NONE); + GridDataBuilder.create(filler).fill().grab().hSpan(2); + + createCloseButton(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // End of hiding from SWT Designer + //$hide<<$ + + private String getVersion() { + LocalPackage platformPackage = mSdkContext.getHandler().getLatestLocalPackageForPrefix(SdkConstants.FD_TOOLS, false, mSdkContext.getProgressIndicator()); + if (platformPackage != null) + return platformPackage.getVersion().toShortString(); + return getToolsVersion(); + } + + private String getToolsVersion() { + Properties p = new Properties(); + File suffix = new File(SdkConstants.FD_TOOLS, SdkConstants.FN_SOURCE_PROP); + FileOp fileOp = FileOpUtils.create(); + InputStream fis = null; + try { + fis = fileOp.newFileInputStream(new File(mSdkContext.getLocation().toString(), suffix.toString())); + p.load(fis); + } catch (IOException ignore) { + } finally { + if (fis != null) + try { + fis.close(); + } catch (IOException ignore) { + } + } + return p.getProperty(PkgProps.PKG_REVISION, ""); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java new file mode 100644 index 00000000..4ee479f3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java @@ -0,0 +1,26 @@ +/* + * 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.sdkuilib.internal.repository; + +/** + * A task that executes updates a monitor to display it's status. + * The task will be run in a separate job. + * @param monitor Progress monitor + */ +public interface ITask { + void run(ITaskMonitor monitor); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java new file mode 100644 index 00000000..40741ddf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java @@ -0,0 +1,35 @@ +/* + * 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.sdkuilib.internal.repository; + + +/** + * A factory starts {@link ITask}s as a job and returns immediately. + * An optional Runnable parameter can be given to run when the job is done. + */ +public interface ITaskFactory { + + /** + * Starts a new task with a new {@link ITaskMonitor}. + * The task will execute asynchronously in a job. + * @param title The title of the task, displayed in the monitor if any. + * @param task The task to run. + * @param onTerminateTask Callback when task done. Can be null + */ + void start(String title, ITask task, Runnable onTerminateTask); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java new file mode 100644 index 00000000..966fc58c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java @@ -0,0 +1,158 @@ +/* + * 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.sdkuilib.internal.repository; + +import com.android.utils.ILogger; + + +/** + * A monitor interface for a {@link ITask}. + *

+ * Depending on the task factory that created the task, there might not be any UI + * or it might not implement all the methods, in which case calling them would be + * a no-op but is guaranteed not to crash. + *

+ * If the task runs in a non-UI worker thread, the task factory implementation + * will take care of the update the UI in the correct thread. The task itself + * must not have to deal with it. + *

+ * A monitor typically has 3 levels of text displayed:
+ * - A title may be present on a task dialog, typically when a task + * dialog is created. This is not covered by this monitor interface.
+ * - A description displays prominent information on what the task + * is currently doing. This is expected to vary over time, typically changing + * with each sub-monitor, and typically only the last description is visible. + * For example an updater would typically have descriptions such as "downloading", + * "installing" and finally "done". This is set using {@link #setDescription}.
+ * - A verbose optional log that can provide more information than the summary + * description and is typically displayed in some kind of scrollable multi-line + * text field so that the user can keep track of what happened. 3 levels are + * provided: error, normal and verbose. An UI may hide the log till an error is + * logged and/or might hide the verbose text unless a flag is checked by the user. + * This is set using {@link #log}, {@link #logError} and {@link #logVerbose}. + *

+ * A monitor is also an {@link ILogger} implementation. + */ +public interface ITaskMonitor extends ILogger { + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + void setDescription(String format, Object...args); + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + void log(String format, Object...args); + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + void logError(String format, Object...args); + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + void logVerbose(String format, Object...args); + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * This method MUST be invoked once before using {@link #incProgress(int)} or + * {@link #getProgress()} or {@link #createSubMonitor(int)}. Callers are + * discouraged from using more than once -- implementations can either discard + * the next calls or behave incoherently. + */ + void setProgressMax(int max); + + /** + * Returns the max value of the progress bar, as last set by {@link #setProgressMax(int)}. + * Returns 0 if the max has never been set yet. + */ + int getProgressMax(); + + /** + * Increments the current value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * Callers MUST use setProgressMax before using this method. + */ + void incProgress(int delta); + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * Callers MUST use setProgressMax before using this method. + */ + int getProgress(); + + /** + * Returns true if the user requested to cancel the operation. + * It is up to the task thread to pool this and exit as soon + * as possible. + */ + boolean isCancelRequested(); + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + ITaskMonitor createSubMonitor(int tickCount); + + /** + * Display a yes/no question dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + boolean displayPrompt(final String title, final String message); + + /** + * Display info dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + void displayInfo(final String title, final String message); + + /** + * Launch an interface which asks for user credentials. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns the user provided credentials. Some fields may be blank if the user + * did not provide any input. + If operation is canceled by user the return value must be null. + */ + UserCredentials displayLoginCredentialsPrompt(String title, String message); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java new file mode 100644 index 00000000..9813cbb5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository; + +import java.util.List; + +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RepoManager.RepoLoadedCallback; + +/** + * Packet containing parameters to call RepoManager load() + * + * + *

public abstract void load(long cacheExpirationMs,
+            @Nullable List onLocalComplete,
+            @Nullable List onSuccess,
+            @Nullable List onError,
+            @NonNull ProgressRunner runner,
+            @Nullable Downloader downloader,
+            @Nullable SettingsController settings,
+            boolean sync);
+        
+ * @author Andrew Bowley + * + * 12-11-2017 + */ +public class LoadPackagesRequest { + + private ProgressRunner runner; + private List onLocalComplete; + private List onSuccess; + private List onError; + + /** + * + */ + public LoadPackagesRequest(ProgressRunner runner) { + this.runner = runner; + } + + public List getOnLocalComplete() { + return onLocalComplete; + } + + public void setOnLocalComplete(List onLocalComplete) { + this.onLocalComplete = onLocalComplete; + } + + public List getOnSuccess() { + return onSuccess; + } + + public void setOnSuccess(List onSuccess) { + this.onSuccess = onSuccess; + } + + public List getOnError() { + return onError; + } + + public void setOnError(List onError) { + this.onError = onError; + } + + public ProgressRunner getRunner() { + return runner; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java new file mode 100644 index 00000000..d7edfd13 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java @@ -0,0 +1,59 @@ +/* + * 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.sdkuilib.internal.repository; + + +import com.android.menubar.IMenuBarCallback; +import com.android.menubar.MenuBarEnhancer; + +import org.eclipse.swt.widgets.Menu; + +/** + * A simple wrapper/delegate around the {@link MenuBarEnhancer}. + * + * The {@link MenuBarEnhancer} and {@link IMenuBarCallback} classes are only + * available when the SwtMenuBar library is available too. This wrapper helps + * {@link SdkUpdaterWindowImpl2} make the call conditional, otherwise the updater + * window class would fail to load when the SwtMenuBar library isn't found. + */ +public abstract class MenuBarWrapper { + + public MenuBarWrapper(String appName, Menu menu) { + MenuBarEnhancer.setupMenu(appName, menu, new IMenuBarCallback() { + @Override + public void onPreferencesMenuSelected() { + MenuBarWrapper.this.onPreferencesMenuSelected(); + } + + @Override + public void onAboutMenuSelected() { + MenuBarWrapper.this.onAboutMenuSelected(); + } + + @Override + public void printError(String format, Object... args) { + MenuBarWrapper.this.printError(format, args); + } + }); + } + + abstract public void onPreferencesMenuSelected(); + + abstract public void onAboutMenuSelected(); + + abstract public void printError(String format, Object... args); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java new file mode 100644 index 00000000..a06effc3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2009-2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.sdkuilib.internal.repository; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; + +/** + * Represents package that we want to install. + *

+ * A new package is a remote package that needs to be downloaded and then + * installed. It can replace an existing local one. It can also depend on another + * (new or local) package, which means the dependent package needs to be successfully + * installed first. Finally this package can also be a dependency for another one. + *

+ * The accepted and rejected flags are used by {@code SdkUpdaterChooserDialog} to follow + * user choices. The installer should never install something that is not accepted. + *

+ * Note: There is currently no logic to support more than one level of + * dependency, either here or in the {@code SdkUpdaterChooserDialog}, since we currently + * have no need for it. + */ +public class PackageInfo implements Comparable { + + private final RemotePackage mNewPackage; + private LocalPackage mReplaced; + private boolean mAccepted; + private boolean mRejected; + + /** + * Creates a new replacement where the {@code newPackage} will replace the + * currently installed {@code replaced} package. + * When {@code newPackage} is not intended to replace anything (e.g. because + * the user is installing a new package not present on her system yet), then + * {@code replace} shall be null. + * + * @param newPackage A "new package" to be installed. This is always an package + * that comes from a remote site. + */ + public PackageInfo(@NonNull RemotePackage newPackage) { + this(newPackage, null); + } + + public PackageInfo(@NonNull RemotePackage newPackage, @Nullable LocalPackage replaced) { + mNewPackage = newPackage; + mReplaced = replaced; + } + + /** + * Returns the "new archive" to be installed. + * This may be null for missing archives. + */ + public RemotePackage getNewPackage() { + return mNewPackage; + } + + /** + * Returns an optional local archive that the new one will replace. + * Can be null if this archive does not replace anything. + */ + public LocalPackage getReplaced() { + return mReplaced; + } + + /** + * Sets whether this package was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public void setAccepted(boolean accepted) { + mAccepted = accepted; + } + + /** + * Returns whether this package was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public boolean isAccepted() { + return mAccepted; + } + + /** + * Sets whether this package was rejected manually by the user. + * An package can neither accepted nor rejected. + */ + public void setRejected(boolean rejected) { + mRejected = rejected; + } + + /** + * Returns whether this package was rejected manually by the user. + * An package can neither accepted nor rejected. + */ + public boolean isRejected() { + return mRejected; + } + + /** + * PackageInfos are compared using ther "new package" ordering. + * + * @see Package#compareTo(Package) + */ + @Override + public int compareTo(PackageInfo rhs) { + if (mNewPackage != null && rhs != null) { + return mNewPackage.compareTo(rhs.mNewPackage); + } + return 0; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallListener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallListener.java new file mode 100644 index 00000000..9243b394 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallListener.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository; + +/** + * @author Andrew Bowley + * + * 30-11-2017 + */ +public interface PackageInstallListener { + void onPackagesInstalled(int count); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallTask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallTask.java new file mode 100644 index 00000000..3d5af712 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInstallTask.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.repository.api.Installer; +import com.android.repository.api.RemotePackage; + +/** + * @author Andrew Bowley + * + * 29-11-2017 + */ +public abstract class PackageInstallTask implements ITask, Runnable { + + private final SdkContext sdkContext; + private final PackageManager packageManager; + private final List packageList; + private final List acceptedRemotes; + private int numInstalled; + + /** + * + */ + public PackageInstallTask(SdkContext sdkContext, List packageList, List acceptedRemotes) { + this.sdkContext = sdkContext; + this.packageManager = sdkContext.getPackageManager(); + this.packageList = packageList; + this.acceptedRemotes = acceptedRemotes; + } + + public int getNumInstalled() { + return numInstalled; + } + + /* (non-Javadoc) + * @see com.android.sdkuilib.internal.repository.ITask#run(com.android.sdkuilib.internal.repository.ITaskMonitor) + */ + @Override + public void run(ITaskMonitor monitor) { + // Assume installation will proceed instead of complicating the install task + sdkContext.getSdkHelper().broadcastPreInstallHook(sdkContext.getSdkLog()); + List rejectedRemotes = new ArrayList<>(); + Iterator iterator = packageList.iterator(); + while (iterator.hasNext()) { + RemotePackage remote = iterator.next(); + if (!acceptedRemotes.contains(remote)) + rejectedRemotes.add(remote); + } + List remotes = new ArrayList<>(); + remotes.addAll(packageList); + if (!rejectedRemotes.isEmpty()) { + String title = "Package licences not accepted"; + StringBuilder builder = new StringBuilder(); + builder.append("The following packages can not be installed since their " + + "licenses or those of the packages they depend on were not accepted:"); + Iterator iterator2 = rejectedRemotes.iterator(); + while(iterator2.hasNext()) + builder.append('\n').append(iterator2.next().getPath()); + if (!acceptedRemotes.isEmpty()) { + builder.append("\n\nContinue installing the remaining packages?"); + if (!monitor.displayPrompt(title, + builder.toString())) + return; + else { + monitor.displayInfo(title, builder.toString()); + return; + } + } + remotes = acceptedRemotes; + } + final int progressPerPackage = 2 * 100; + monitor.setProgressMax(1 + remotes.size() * progressPerPackage); + monitor.setDescription("Preparing to install packages"); + for (RemotePackage remotePackage : remotes) { + int nextProgress = monitor.getProgress() + progressPerPackage; + Installer installer = packageManager.createInstaller(remotePackage); + if (packageManager.applyPackageOperation(installer)) { + ++numInstalled; + } else { + // there was an error, abort. + monitor.error(null, "Install of package failed due to an error"); + monitor.setProgressMax(0); + break; + } + if (monitor.isCancelRequested()) { + break; + } + monitor.incProgress(nextProgress - monitor.getProgress()); + } + if (numInstalled > 0) + sdkContext.getSdkHelper().broadcastPostInstallHook(sdkContext.getSdkLog()); + /* Post package install operations used to give the user an opportuning to restart ADB + * and advise check for ADT Updates + if (installedAddon || installedPlatformTools) { + // We need to restart ADB. Actually since we don't know if it's even + // running, maybe we should just kill it and not start it. + // Note: it turns out even under Windows we don't need to kill adb + // before updating the tools folder, as adb.exe is (surprisingly) not + // locked. + + askForAdbRestart(monitor); + } + + if (installedTools) { + notifyToolsNeedsToBeRestarted(flags); + } + */ + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java new file mode 100644 index 00000000..ce69750c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java @@ -0,0 +1,187 @@ +package com.android.sdkuilib.internal.repository; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.repository.api.Downloader; +import com.android.repository.api.Installer; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.PackageOperation; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager; +import com.android.repository.api.RepoManager.RepoLoadedCallback; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.io.FileOpUtils; +import com.android.repository.io.impl.FileSystemFileOp; +import com.android.sdklib.repository.installer.SdkInstallerUtil; +import com.android.sdklib.repository.legacy.LegacyDownloader; +import com.google.common.collect.Maps; + +public class PackageManager { + + public interface LocalPackageHandler + { + void onPackageLoaded(LocalPackage localPackage); + } + + public interface RemotePackageHandler + { + void onPackageLoaded(RemotePackage remotePackage); + } + + public interface UpdatablePackageHandler + { + void onPackageLoaded(UpdatablePackage updatablePackage); + } + + /** + * Map from {@code path} (the unique ID of a package) to {@link UpdatablePackage}, including all + * packages installed or available. + */ + private final Map consolidatedPkgs = Maps.newTreeMap(); + private final SdkContext sdkContext; + private final Downloader downloader; + private RepositoryPackages packages; + + public PackageManager(SdkContext sdkContext) + { + this.sdkContext = sdkContext; + downloader = downloaderInstance(); + } + + public Downloader getDownloader() + { + return downloader; + } + + public UpdatablePackage getPackageById(String id) + { + return consolidatedPkgs.get(id); + } + + public boolean loadLocalPackages(LocalPackageHandler handler) + { + sdkContext.getRepoManager().loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + if (sdkContext.hasError()) + return false; + + for (LocalPackage local : getRepositoryPackages().getLocalPackages().values()) + handler.onPackageLoaded(local); + return true; + } + + public boolean loadRemotePackages(RemotePackageHandler handler) + { + sdkContext.getRepoManager().loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + if (sdkContext.hasError()) + return false; + + for (RemotePackage remote : getRepositoryPackages().getRemotePackages().values()) + handler.onPackageLoaded(remote); + return true; + } + + public boolean loadUpdatablePackages(UpdatablePackageHandler handler) + { + if (sdkContext.hasError()) + return false; + for (UpdatablePackage updatable : getRepositoryPackages().getUpdatedPkgs()) + handler.onPackageLoaded(updatable); + return true; + } + + public boolean loadConsolidatedPackages(UpdatablePackageHandler handler) + { + if (sdkContext.hasError()) + return false; + consolidatedPkgs.clear(); + consolidatedPkgs.putAll(getRepositoryPackages().getConsolidatedPkgs()); + for (UpdatablePackage updatable : consolidatedPkgs.values()) + handler.onPackageLoaded(updatable); + return true; + } + + public RepositoryPackages getRepositoryPackages() + { + if (packages == null) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + packages = repoManager.getPackages(); + } + return packages; + } +/* + public abstract void load(long cacheExpirationMs, + @Nullable List onLocalComplete, + @Nullable List onSuccess, + @Nullable List onError, + @NonNull ProgressRunner runner, + @Nullable Downloader downloader, + @Nullable SettingsController settings, + boolean sync); + */ + + public void requestRepositoryPackages(LoadPackagesRequest loadPackagesRequest) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.load(0, + loadPackagesRequest.getOnLocalComplete(), + loadPackagesRequest.getOnSuccess(), + loadPackagesRequest.getOnError(), + loadPackagesRequest.getRunner(), + downloader, + sdkContext.getSettings(), + false); + } + + public RepositoryPackages getRepositoryPackages( + @Nullable List onLocalComplete, + @Nullable List onSuccess, + @Nullable List onError, + @NonNull ProgressRunner runner) + { + if (packages == null) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.load(0, onLocalComplete, onSuccess, onError, runner, downloader, sdkContext.getSettings(), true); + packages = repoManager.getPackages(); + } + return packages; + } + + public void setPackages(RepositoryPackages packages) { + this.packages = packages; + } + + protected Installer createInstaller(RemotePackage remotePackage) { + return SdkInstallerUtil.findBestInstallerFactory(remotePackage, sdkContext.getHandler()) + .createInstaller(remotePackage, sdkContext.getRepoManager(), downloader, sdkContext.getFileOp()); + + } + + private Downloader downloaderInstance() + { + FileSystemFileOp fop = (FileSystemFileOp)FileOpUtils.create(); + return new LegacyDownloader(fop, sdkContext.getSettings()); + } + + boolean applyPackageOperation( + @NonNull PackageOperation operation) { + ProgressIndicator progressIndicator = sdkContext.getProgressIndicator(); + return operation.prepare(progressIndicator) && operation.complete(progressIndicator); + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java new file mode 100644 index 00000000..d080f229 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java @@ -0,0 +1,88 @@ +package com.android.sdkuilib.internal.repository; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.repository.api.Channel; +import com.android.repository.api.SettingsController; +import com.android.repository.io.FileOp; +import com.android.repository.io.FileOpUtils; +import com.android.repository.io.impl.FileSystemFileOp; +import com.android.utils.ILogger; + +public class Settings implements SettingsController { + + private static final String SETTINGS_FILENAME = "androidtool.cfg"; + public static final String FORCE_HTTP_KEY = "sdkman.force.http"; + + private int mChannel = 0; + private final FileOp mFileOp; + private final Properties mProperties; + + public Settings() + { + mProperties = new Properties(); + mFileOp = (FileSystemFileOp)FileOpUtils.create(); + } + + public boolean getEnablePreviews() { + return true; + } + + @Override + public boolean getForceHttp() { + return Boolean.parseBoolean(this.mProperties.getProperty(FORCE_HTTP_KEY)); + } + + @Override + public void setForceHttp(boolean force) { + mProperties.setProperty(FORCE_HTTP_KEY, Boolean.toString(force)); + } + + @Nullable + @Override + public Channel getChannel() { + return Channel.create(mChannel); + } + + public boolean initialize(ILogger logger) { + try { + loadSettings(); + } catch (AndroidLocationException | IOException e) { + logger.error(e, "Error initializing SDK settings"); + return false; + } + return true; + } + + + private void loadSettings() throws AndroidLocationException, IOException + { + String folder = AndroidLocation.getFolder(); + File file = new File(folder, SETTINGS_FILENAME); + + mProperties.clear(); + mProperties.load(mFileOp.newFileInputStream(file)); + + //setShowUpdateOnly(getShowUpdateOnly()); + //setSetting("sdkman.ask.adb.restart", getAskBeforeAdbRestart()); + //setSetting("sdkman.use.dl.cache", getUseDownloadCache()); + //setSetting("sdkman.enable.previews2", getEnablePreviews()); + } + + public Settings copy() { + Settings copy = new Settings(); + // Load is not expected to fail. Copy will contain defaults if it does. + try { + copy.loadSettings(); + } catch (AndroidLocationException e) { + } catch (IOException e) { + } + return copy; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java new file mode 100644 index 00000000..029fd936 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java @@ -0,0 +1,279 @@ +/* + * 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.sdkuilib.internal.repository; + +//import com.android.sdklib.repository.legacy.remote.internal.DownloadCache; +//import com.android.sdklib.repository.legacy.remote.internal.DownloadCache.Strategy; +//import com.android.sdklib.internal.repository.updater.ISettingsPage; +//import com.android.sdklib.internal.repository.updater.SettingsController; +//import com.android.sdklib.util.FormatUtils; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.andmore.sdktool.SdkContext; +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.Point; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.Properties; + + +public class SettingsDialog extends UpdaterBaseDialog { + + + // data members +// private final DownloadCache mDownloadCache = new DownloadCache(Strategy.SERVE_CACHE); +// private final SettingsController mSettingsController; +// private SettingsChangedCallback mSettingsChangedCallback; + + // UI widgets +// private Text mTextProxyServer; +// private Text mTextProxyPort; + // TODO - Add channel configuration + private Text mTextChannel; +// private Button mCheckUseCache; + private Button mCheckForceHttp; +// private Button mCheckAskAdbRestart; +// private Button mCheckEnablePreviews; + + private SelectionAdapter mApplyOnSelected = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + applyNewSettings(); //$hide$ + } + }; + + private ModifyListener mApplyOnModified = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + applyNewSettings(); //$hide$ + } + }; + + public SettingsDialog(Shell parentShell, SdkContext sdkContext) { + super(parentShell, sdkContext, "Settings" /*title*/); + } + + @Override + protected void createShell() { + super.createShell(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(450, 370)); + shell.setSize(450, 400); + } + + @Override + protected void createContents() { + super.createContents(); + Shell shell = getShell(); + /* + Group group = new Group(shell, SWT.NONE); + group.setText("Proxy Settings"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(2); + + Label label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("HTTP Proxy Server"); + String tooltip = "The hostname or IP of the HTTP & HTTPS proxy server to use (e.g. proxy.example.com).\n" + + "When empty, the default Java proxy setting is used."; + label.setToolTipText(tooltip); + mTextProxyServer = new Text(group, SWT.BORDER); + GridDataBuilder.create(mTextProxyServer).hFill().hGrab().vCenter(); + mTextProxyServer.addModifyListener(mApplyOnModified); + mTextProxyServer.setToolTipText(tooltip); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("HTTP Proxy Port"); + tooltip = "The port of the HTTP & HTTPS proxy server to use (e.g. 3128).\n" + + "When empty, the default Java proxy setting is used."; + label.setToolTipText(tooltip); + + mTextProxyPort = new Text(group, SWT.BORDER); + GridDataBuilder.create(mTextProxyPort).hFill().hGrab().vCenter(); + mTextProxyPort.addModifyListener(mApplyOnModified); + mTextProxyPort.setToolTipText(tooltip); + + // ---- + group = new Group(shell, SWT.NONE); + group.setText("Manifest Cache"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(3); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("Directory:"); + + Text text = new Text(group, SWT.NONE); + GridDataBuilder.create(text).hFill().hGrab().vCenter().hSpan(2); + text.setEnabled(false); + text.setText(mDownloadCache.getCacheRoot().getAbsolutePath()); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("Current Size:"); + + mTextCacheSize = new Text(group, SWT.NONE); + GridDataBuilder.create(mTextCacheSize).hFill().hGrab().vCenter().hSpan(2); + mTextCacheSize.setEnabled(false); + updateDownloadCacheSize(); + + mCheckUseCache = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckUseCache).vCenter().hSpan(1); + mCheckUseCache.setText("Use download cache"); + mCheckUseCache.setToolTipText("When checked, small manifest files are cached locally.\n" + + "Large binary files are never cached locally."); + mCheckUseCache.addSelectionListener(mApplyOnSelected); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab().hSpan(1); + + Button button = new Button(group, SWT.PUSH); + GridDataBuilder.create(button).vCenter().hSpan(1); + button.setText("Clear Cache"); + button.setToolTipText("Deletes all cached files."); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mDownloadCache.clearCache(); + updateDownloadCacheSize(); + } + }); +*/ + // ---- + Group group = new Group(shell, SWT.NONE); + group.setText("Options"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(2); + + mCheckForceHttp = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckForceHttp).hFill().hGrab().vCenter().hSpan(2); + mCheckForceHttp.setText("Force https://... sources to be fetched using http://..."); + mCheckForceHttp.setToolTipText( + "If you are not able to connect to the official Android repository using HTTPS,\n" + + "enable this setting to force accessing it via HTTP."); + mCheckForceHttp.addSelectionListener(mApplyOnSelected); +/* + mCheckAskAdbRestart = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckAskAdbRestart).hFill().hGrab().vCenter().hSpan(2); + mCheckAskAdbRestart.setText("Ask before restarting ADB"); + mCheckAskAdbRestart.setToolTipText( + "When checked, the user will be asked for permission to restart ADB\n" + + "after updating an addon-on package or a tool package."); + mCheckAskAdbRestart.addSelectionListener(mApplyOnSelected); + mCheckEnablePreviews = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckEnablePreviews).hFill().hGrab().vCenter().hSpan(2); + mCheckEnablePreviews.setText("Enable Preview Tools"); + mCheckEnablePreviews.setToolTipText( + "When checked, the package list will also display preview versions of the tools.\n" + + "These are optional future release candidates that the Android tools team\n" + + "publishes from time to time for early feedback."); + mCheckEnablePreviews.addSelectionListener(mApplyOnSelected); +*/ + + Label filler = new Label(shell, SWT.NONE); + GridDataBuilder.create(filler).hFill().hGrab(); + + createCloseButton(); + } + + @Override + protected void postCreate() { + super.postCreate(); + loadSettings(); + } + + @Override + protected void close() { + // Dissociate this page from the controller + //mSettingsController.setSettingsPage(null); + super.close(); + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** Loads settings from the given {@link Properties} container and update the page UI. */ + public void loadSettings() { + Settings settings = mSdkContext.getSettings(); + //mTextProxyServer.setText(inSettings.getProperty(KEY_HTTP_PROXY_HOST, "")); //$NON-NLS-1$ + //mTextProxyPort.setText( inSettings.getProperty(KEY_HTTP_PROXY_PORT, "")); //$NON-NLS-1$ + mCheckForceHttp.setSelection(settings.getForceHttp()); + //mCheckAskAdbRestart.setSelection( + // Boolean.parseBoolean(inSettings.getProperty(KEY_ASK_ADB_RESTART))); + //mCheckUseCache.setSelection( + // Boolean.parseBoolean(inSettings.getProperty(KEY_USE_DOWNLOAD_CACHE))); + //mCheckEnablePreviews.setSelection(false); + + } + + /** Called by the application to retrieve settings from the UI and store them in + * the given {@link Properties} container. */ + public void retrieveSettings() { + Settings settings = mSdkContext.getSettings(); + //outSettings.setProperty(KEY_HTTP_PROXY_HOST, mTextProxyServer.getText()); + //outSettings.setProperty(KEY_HTTP_PROXY_PORT, mTextProxyPort.getText()); + settings.setForceHttp(mCheckForceHttp.getSelection()); + //outSettings.setProperty(KEY_ASK_ADB_RESTART, + // Boolean.toString(mCheckAskAdbRestart.getSelection())); + //outSettings.setProperty(KEY_USE_DOWNLOAD_CACHE, + // Boolean.toString(mCheckUseCache.getSelection())); + //outSettings.setProperty(KEY_ENABLE_PREVIEWS, + // Boolean.toString(mCheckEnablePreviews.getSelection())); + + } + + /** + * Called by the application to give a callback that the page should invoke when + * settings must be applied. The page does not apply the settings itself, instead + * it notifies the application. + */ + /* + public void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback) { + mSettingsChangedCallback = settingsChangedCallback; + } +*/ + /** + * Callback invoked when user touches one of the settings. + * There is no "Apply" button, settings are applied immediately as they are changed. + * Notify the application that settings have changed. + */ + private void applyNewSettings() { + retrieveSettings(); + } +/* + private void updateDownloadCacheSize() { + long size = mDownloadCache.getCurrentSize(); + String str = FormatUtils.byteSizeToString(size); + mTextCacheSize.setText(str); + } +*/ + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java new file mode 100644 index 00000000..f1123f1f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java @@ -0,0 +1,106 @@ +/* + * 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.sdkuilib.internal.repository; + +import com.android.SdkConstants; +import org.eclipse.andmore.base.resources.ImageFactory; +//import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Shell; + + + +/** + * Base class for auxiliary dialogs shown in the updater (for example settings, + * about box or add-on site.) + */ +public abstract class UpdaterBaseDialog extends SwtBaseDialog { + public static final String APP_NAME = "Android SDK Manager"; + + protected final SdkContext mSdkContext; + + protected UpdaterBaseDialog(Shell parentShell, SdkContext sdkContext, String title) { + super(parentShell, + SWT.APPLICATION_MODAL, + String.format("%1$s - %2$s", APP_NAME, title)); //$NON-NLS-1$ + mSdkContext = sdkContext; + } + + public SdkContext getSdkContext() { + return mSdkContext; + } + + /** + * Initializes the shell with a 2-column Grid layout. + * Caller should use {@link #createCloseButton()} to inject the + * close button at the bottom of the dialog. + */ + @Override + protected void createContents() { + Shell shell = getShell(); + setWindowImage(shell); + + GridLayoutBuilder.create(shell).columns(2); + } + + protected void createCloseButton() { + Button close = new Button(getShell(), SWT.PUSH); + close.setText("Close"); + GridDataBuilder.create(close).hFill().vBottom(); + close.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + close(); + } + }); + } + + @Override + protected void postCreate() { + // pass + } + + @Override + protected void close() { + super.close(); + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java new file mode 100644 index 00000000..c3e55d1c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java @@ -0,0 +1,50 @@ +/* + * 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.sdkuilib.internal.repository; + +/** + * User authentication details + */ +public class UserCredentials { + private final String mUserName; + private final String mPassword; + private final String mWorkstation; + private final String mDomain; + + public UserCredentials(String userName, String password, String workstation, String domain) { + mUserName = userName; + mPassword = password; + mWorkstation = workstation; + mDomain = domain; + } + + public String getUserName() { + return mUserName; + } + + public String getPassword() { + return mPassword; + } + + public String getWorkstation() { + return mWorkstation; + } + + public String getDomain() { + return mDomain; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java new file mode 100644 index 00000000..a6bd1939 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.avd; + +import java.util.Map; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkVersionInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.content.PackageType; + +/** + * Contains and AvdInfo object and details extracted from it suitable for application use. + * Also contains the target assigned to the AvdInfo object to match it's Android version + * @author Andrew Bowley + * + * 15-11-2017 + */ +public class AvdAgent { + + private static final String BLANK = ""; + private final AvdInfo avd; + private final IAndroidTarget target; + private String deviceName; + private String deviceMfctr; + private String path; + private SystemImageInfo systemImageInfo; + private String platformVersion; + private String versionWithCodename; + private AndroidVersion androidVersion; + private PackageType packageType; + private String vendor; + private String targetDisplayName; + private String skin; + private String sdcard; + private String snapshot; + + /** + * + */ + public AvdAgent(IAndroidTarget target, AvdInfo avd) { + this.target = target; + this.avd = avd; + path = avd.getDataFolderPath(); + systemImageInfo = new SystemImageInfo(avd); + vendor = BLANK; + targetDisplayName = BLANK; + init(); + } + + public AvdInfo getAvd() { + return avd; + } + + public IAndroidTarget getTarget() + { + return target; + } + + public String getDeviceName() { + return deviceName; + } + + public String getDeviceMfctr() { + return deviceMfctr; + } + + public String getPath() { + return path; + } + + public SystemImageInfo getSystemImageInfo() { + return systemImageInfo; + } + + public String getPlatformVersion() { + return platformVersion; + } + + public String getVersionWithCodename() { + return versionWithCodename; + } + + public AndroidVersion getAndroidVersion() { + return androidVersion; + } + + public PackageType getPackageType() { + return packageType; + } + + public String getVendor() { + return vendor; + } + + public String getTargetFullName() { + return target.getFullName(); + } + + public String getTargetVersionName() { + String platform = target.getVersionName(); + return platform != null ? platform : BLANK; + } + + public String getTargetDisplayName() { + return targetDisplayName; + } + + public String getSkin() { + return skin; + } + + public String getSdcard() { + return sdcard; + } + + public String getSnapshot() { + return snapshot; + } + + public String getPrettyAbiType() { + return AvdInfo.getPrettyAbiType(avd); + } + + private void init() + { + deviceName = avd.getProperties().get(AvdManager.AVD_INI_DEVICE_NAME); + deviceMfctr = avd.getProperties().get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + if (deviceName == null) { + deviceName = BLANK; + deviceMfctr = BLANK; + } + else if (deviceMfctr == null) + deviceMfctr = BLANK; + androidVersion = systemImageInfo.getAndroidVersion(); + versionWithCodename = SdkVersionInfo + .getVersionWithCodename(androidVersion); + platformVersion = SdkVersionInfo.getVersionString(androidVersion.getApiLevel()); + PackageType packageType = systemImageInfo.getPackageType(); + if ((packageType == PackageType.system_images) || (packageType == PackageType.add_ons)) { + vendor = systemImageInfo.getVendor(); + } + if (target.isPlatform()) { + targetDisplayName = String.format(" API: %s", versionWithCodename); + } else { + targetDisplayName = + String.format("Target: %s\n" + + " Based on %s)", + target.getFullName(), + target.getParent().getFullName()); + } + // Some extra values. + Map properties = avd.getProperties(); + skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard == null) + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); + if (sdcard == null) + sdcard = BLANK; + snapshot = properties.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshot == null) + snapshot = BLANK; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java new file mode 100644 index 00000000..88d92c18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.avd; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.repository.api.ProgressIndicator; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdklib.repository.targets.AddonTarget; +import com.android.sdklib.repository.targets.PlatformTarget; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.content.PackageType; + +/** + * Container for Android targets and System images which relates them by Andriod version using + * AndroidTargetHash .getPlatformHashString() following AVD Manager practice. + * @author Andrew Bowley + * + * 16-11-2017 + */ +public class SdkTargets { + /** All available targets */ + private final List targets = new ArrayList<>(); + /** All available system images */ + private final Collection sysImages; + /** Maps system image to target by platform hash */ + private final Map targetMap = new HashMap<>(); + + /** + * Construct SdkTargets object + * @param sdkContext The SDK context containing reference to AndroidSdkHandler object + */ + public SdkTargets(SdkContext sdkContext) { + // Use AndroidTargetManager to get targets + ProgressIndicator progress = sdkContext.getProgressIndicator(); + targets.addAll( + sdkContext.getHandler().getAndroidTargetManager(progress) + .getTargets(progress)); + Collections.sort(targets, new Comparator(){ + + @Override + public int compare(IAndroidTarget target1, IAndroidTarget target2) { + return getApiLevel(target1) - getApiLevel(target2); + }}); + // Use SystemImageManager to get system images + sysImages = + sdkContext.getHandler().getSystemImageManager(progress).getImages(); + Iterator iterator = sysImages.iterator(); + while(iterator.hasNext()) { + SystemImage systemImage = iterator.next(); + IAndroidTarget target = mapTarget(systemImage); + if (target != null) + targetMap.put(systemImage, target); + } + + } + + /** + * Returns all available system images for given target + * @param target Android target + * @return sorted list of system images + */ + public List getSystemImages(IAndroidTarget target) { + List systemImages = new ArrayList<>(); + for (SystemImage systemImage: sysImages) { + if (filterOnApi(systemImage, target)) + systemImages.add(systemImage); + } + // Sort + Collections.sort(systemImages, new Comparator(){ + + @Override + public int compare(SystemImage sysImage1, SystemImage sysImage2) { + return sysImage1.compareTo(sysImage2); + }}); + return systemImages; + } + + /** + * Returns map of system image to target by platform hash + * @return map + */ + public Map getTargetMap() + { + return Collections.unmodifiableMap(targetMap); + } + + /** + * Returns all available targets + * @return IAndroidTarget collection + * @ss {@link #getSystemImageTargets()} + */ + public Collection getTargets() { + return targets; + } + + /** + * Returns all targets with available system images + * @return IAndroidTarget collection + */ + public Collection getSystemImageTargets() { + Set systemImageTargets = new TreeSet<>(); + for (SystemImage systemImage: sysImages) { + for (IAndroidTarget target: targets) { + if (filterOnApi(systemImage, target)) { + systemImageTargets.add(target); + break; + } + } + } + return systemImageTargets; + } + + /** + * Returns all available system images + * @return SystemImage collection + */ + public Collection getSysImages() { + return sysImages; + } + + /** + * Return target matching Android version of given system image + * @param systemImage SystemImage object + * @return IAndroidTarget object + */ + public IAndroidTarget getTargetForSysImage(SystemImage systemImage) { + return targetMap.get(systemImage); + } + + public IAndroidTarget getTargetForAndroidVersion(AndroidVersion androidVersion) { + for (IAndroidTarget target: targetMap.values()) { + if (filterOnApi(androidVersion, target)) + return target; + } + return null; + } + + private IAndroidTarget mapTarget(SystemImage systemImage) { + PackageType packageType = null; + IdDisplay vendorId = IdDisplay.create("", ""); + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + if (details instanceof DetailsTypes.PlatformDetailsType) { + packageType = PackageType.platforms; + } else if (details instanceof DetailsTypes.SysImgDetailsType) { + packageType = PackageType.system_images; + vendorId = ((DetailsTypes.SysImgDetailsType) details).getVendor(); + } else if (details instanceof DetailsTypes.AddonDetailsType) { + packageType = PackageType.add_ons; + vendorId = ((DetailsTypes.AddonDetailsType) details).getVendor(); + } + for (IAndroidTarget target: targets) + { + if (filterOnApi(systemImage, target)) + { + if ((packageType == PackageType.add_ons) && + !target.isPlatform() && + target.getVendor().equals(vendorId.getId())) + return target; + else if (target.isPlatform()) + return target; + } + } + return null; + } + + private boolean filterOnApi(SystemImage systemImage, IAndroidTarget target) + { + // AVD Manager, for each AVD, stores just platform version as hash, so this defines the system image/target association + String imageApiHash = AndroidTargetHash.getPlatformHashString(systemImage.getAndroidVersion()); + String targetHash = AndroidTargetHash.getPlatformHashString(target.getVersion()); + return imageApiHash.equals(targetHash); + } + + private boolean filterOnApi(AndroidVersion version, IAndroidTarget target) + { + // AVD Manager, for each AVD, stores just platform version as hash, so this defines the system image/target association + String imageApiHash = AndroidTargetHash.getPlatformHashString(version); + String targetHash = AndroidTargetHash.getPlatformHashString(target.getVersion()); + return imageApiHash.equals(targetHash); + } + + private int getApiLevel(IAndroidTarget target) { + if (target.isPlatform()) { + PlatformTarget plaformTarget = (PlatformTarget)target; + return plaformTarget.getVersion().getApiLevel(); + } + AddonTarget addonTarget = (AddonTarget)target; + return addonTarget.getParent().getVersion().getApiLevel(); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SystemImageInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SystemImageInfo.java new file mode 100644 index 00000000..7f31cf41 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SystemImageInfo.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.avd; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.content.PackageType; + +/** + * System image wrapper which allows an AVD configuration to reference an image which does not exist in current SDK + * @author Andrew Bowley + * + * 01-12-2017 + */ +public class SystemImageInfo { + private static final String BLANK = ""; + + private SystemImage systemImage; + private AndroidVersion avdAndroidVersion; + + /** + * Construct SystemImageInfo object for given AVD info + */ + public SystemImageInfo(AvdInfo avd) { + this.systemImage = (SystemImage) avd.getSystemImage(); + avdAndroidVersion = avd.getAndroidVersion(); + } + + public boolean hasSystemImage() { + return systemImage != null; + } + + public SystemImage getSystemImage() { + return systemImage; + } + + public void setSystemImage(SystemImage systemImage) { + this.systemImage = systemImage; + } + + public AndroidVersion getAndroidVersion() { + if (hasSystemImage()) { + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + return details.getAndroidVersion(); + } + return avdAndroidVersion; + } + + public PackageType getPackageType() { + if (hasSystemImage()) { + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + if (details instanceof DetailsTypes.PlatformDetailsType) { + return PackageType.platforms; + } else if (details instanceof DetailsTypes.SysImgDetailsType) { + return PackageType.system_images; + } else if (details instanceof DetailsTypes.AddonDetailsType) { + return PackageType.add_ons; + } + } + return PackageType.platforms; + } + + public String getVendor() { + if (hasSystemImage()) { + PackageType packageType = getPackageType(); + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + if (packageType == PackageType.system_images) { + IdDisplay vendorId = ((DetailsTypes.SysImgDetailsType) details).getVendor(); + if (vendorId != null) { + return vendorId.getDisplay(); + } + } else if (packageType == PackageType.add_ons) { + return ((DetailsTypes.AddonDetailsType) details).getVendor().getDisplay(); + } + } + return BLANK; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java new file mode 100644 index 00000000..b36ef597 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java @@ -0,0 +1,9 @@ +package com.android.sdkuilib.internal.repository.content; + +public enum CategoryKeyType { + TOOLS, + TOOLS_PREVIEW, + API, + EXTRA, + GENERIC +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java new file mode 100644 index 00000000..8070a0b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.sdkuilib.internal.repository.content; + +import java.util.Collections; +import java.util.List; + +/** + * Tree node which provides the text, font and image for the label of a given tree element + * @author Andrew Bowley + * + */ +public class INode { + /** Font can be normal or italic */ + static enum LabelFont { normal, italic } + + static final List EMPTY_NODE_LIST; + static final String VOID = ""; + + static + { + EMPTY_NODE_LIST = Collections.emptyList(); + } + + /** Flag to mirror node checkbox state */ + protected boolean isChecked; + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element, or VOID if there is no image for the given object + */ + public String getImage(Object element, int columnIndex) { + return VOID; + } + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element, or VOID if there is no text label for the given object + */ + public String getText(Object element, int columnIndex) { + return VOID; + } + + /** + * Provides a font for the given element at index columnIndex. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return LabelFont.normal or LabelFont.italic + */ + public LabelFont getFont(Object element, int columnIndex) { + return LabelFont.normal; + } + + /** + * Get the text displayed in the tool tip for given element + * @param element Target + * @return the tooltop text, or VOID for no text to display + */ + public String getToolTipText(Object element) { + return VOID; + } + + /** + * Returns list of descendents + * @return INode list + */ + public List getChildren() { + return EMPTY_NODE_LIST; + } + + /** + * Returns checkbox state + * @return boolean + */ + public boolean isChecked() { + return isChecked; + } + + /** + * Sets checkbox state + * @param isChecked Value to set checkbox on next refresh + */ + public void setChecked(boolean isChecked) { + this.isChecked = isChecked; + } + + /** + * Mark item as checked according to given criteria. Force uncheck if no criteria specified. + * @param selectUpdates If true, select all update packages + * @param topApiLevel If greater than 0, select platform packages of this api level + */ + public void checkSelections( + boolean selectUpdates, + int topApiLevel) + { + } + + /** + * Mark item as deleted. This is a transient state on the path to removal from the collection to which it belongs + */ + public void markDeleted() + { + } + + /** + * Returns true if item has been marked for deletion + * @return boolean + */ + public boolean isDeleted() + { + return false; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java new file mode 100644 index 00000000..5af460dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java @@ -0,0 +1,28 @@ +package com.android.sdkuilib.internal.repository.content; + +/** + * Information about a package type + */ +public class MetaPackage { + + private final PackageType mPackageType; + private final String mIconResource; + + public MetaPackage(PackageType packageType, String iconResource) { + this.mPackageType = packageType; + this.mIconResource = iconResource; + } + + public PackageType getPackageType() { + return mPackageType; + } + + public String getIconResource() { + return mIconResource; + } + + public String getName() + { + return mPackageType.toString().replaceAll("_", "-"); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java new file mode 100644 index 00000000..f84ef688 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java @@ -0,0 +1,287 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.SdkConstants; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.TypeDetails; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdkuilib.internal.repository.PackageManager.UpdatablePackageHandler; +import com.android.sdkuilib.internal.repository.content.UpdateOp.SdkSource; + +public class PackageAnalyser { + public static final String GENERIC = "generic"; + + private final SdkContext mSdkContext; + private final UpdateOpApi mOpApi; + private final Map mMetaPackageMap = new HashMap<>(); + + /** + * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}. + */ + public static enum PkgState { INSTALLED, NEW, DELETED }; + + public PackageAnalyser(SdkContext sdkContext) + { + this.mSdkContext = sdkContext; + mOpApi = new UpdateOpApi(); + initMetaPackages(); + } + + public MetaPackage getMetaPackage(String name) + { + return mMetaPackageMap.get(name); + } + + /** + * Removes all the internal state and resets the object. + * Useful for testing. + */ + public void clear() { + mOpApi.clear(); + } + + /** + * Mark all new and update PkgItems as checked. + * + * @param selectNew If true, select all new packages (except the rc/preview ones). + * @param selectUpdates If true, select all update packages. + * @param selectTop If true, select the top platform. All new packages are selected, excluding system images and + * rc/preview. Packages to update are selected regardless. + * @param currentPlatform The {@link SdkConstants#currentPlatform()} value. + */ + public void checkNewUpdateItems( + boolean selectUpdates, + boolean selectTop) { + int apiLevel = 0; + if (selectTop) { + for (PkgCategory cat: mOpApi.getCategories()) + { + // Find first API category to get top API + if (cat.getKeyType() == CategoryKeyType.API) { + PkgCategoryApi pkgCategoryApi = (PkgCategoryApi)cat; + apiLevel = pkgCategoryApi.getKeyValue().getApiLevel(); + break; + } + } + } + for (PkgCategory cat: mOpApi.getCategories()) + checkNode(cat, selectUpdates, apiLevel, 0); + } + + private void checkNode(INode node, boolean selectUpdates, int topApiLevel, int level) + { + node.checkSelections(selectUpdates, topApiLevel); + for (INode child: node.getChildren()) + checkNode(child, selectUpdates, topApiLevel, level+ 1); + } + + /** + * Mark all PkgItems as not checked. + */ + public void uncheckAllItems() { + for (PkgCategory cat: mOpApi.getCategories()) + checkNode(cat, false, 0, 0); + } + + public List> getApiCategories() { + return mOpApi.getCategories(); + } + + public List getAllPkgItems() { + List items = new ArrayList(); + + List> cats = getApiCategories(); + synchronized (cats) { + for (PkgCategory cat : cats) { + items.addAll(cat.getItems()); + } + } + return items; + } + + public void removeDeletedNodes() { + for (PkgCategory cat: mOpApi.getCategories()) + remoteDeleted(cat, 0); + } + + private void remoteDeleted(INode node, int level) { + List removeList = null; + for (INode childNode: node.getChildren()) { + if (childNode.isDeleted()) { + if (removeList == null) + removeList = new ArrayList<>(); + removeList.add(childNode); + } + remoteDeleted(childNode, level + 1); + } + if (removeList != null) { + for (INode removeNode: removeList) { + node.getChildren().remove(removeNode); + } + } + } + + public static String getNameFromPath(String path) + { + int pos = path.indexOf(RepoPackage.PATH_SEPARATOR); + return pos == -1 ? path : path.substring(0, pos); + } + + public static AndroidVersion getAndroidVersion(RepoPackage pkg) { + TypeDetails details = pkg.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getAndroidVersion(); + } + return null; + } + + private void initMetaPackages() { + MetaPackage metaPackage = new MetaPackage(PackageType.tools, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.platform_tools, "platformtool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.build_tools, "buildtool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.platforms, "platform_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.add_ons, "addon_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.system_images, "sysimg_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.sources, "source_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.samples, "source_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.docs, "doc_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.extras, "extra_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.emulator, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.cmake, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.lldb, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.ndk_bundle, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.patcher, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.generic, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + } + + public void loadPackages() { + UpdatablePackageHandler updateHandler = new UpdatablePackageHandler(){ + + @Override + public void onPackageLoaded(UpdatablePackage updatePackage) { + updateApiItem(updatePackage); + }}; + // All previous entries must be deleted or duplicates will occur + mOpApi.getCategories().clear(); + mSdkContext.getPackageManager().loadConsolidatedPackages(updateHandler); + mOpApi.sortCategoryList(); + } + + private void updateApiItem(UpdatablePackage updatePackage) { + LocalPackage local = updatePackage.getLocal(); + RemotePackage remote = updatePackage.getRemote(); + PkgCategory cat = null; + PkgItem item = null; + if (local != null) { + cat = getPkgCategory(local); + item = new PkgItem(local, metaPackageFromPackage(local), PkgState.INSTALLED); + if ((remote != null) && !local.getVersion().equals(remote.getVersion())) + item.setUpdatePkg(updatePackage); + } + else { + cat = getPkgCategory(remote); + item = getPkgItem(remote); + } + cat.getItems().add(item); + } + + private PkgItem getPkgItem(RemotePackage remote) { + PkgItem pkgItem = new PkgItem(remote, metaPackageFromPackage(remote), PkgState.NEW); + return pkgItem; + } + + private PkgCategory getPkgCategory(RepoPackage pkg) + { + List> cats = mOpApi.getCategories(); + CategoryKeyType catKeyType = mOpApi.getCategoryKeyType(pkg); + PkgCategory cat = null; + AndroidVersion catKeyValue = null; + switch (catKeyType) + { + case API: + catKeyValue = mOpApi.getCategoryKeyValue(pkg); + cat = findCurrentCategory(cats, catKeyType, catKeyValue); + break; + default: + cat = findCurrentCategory(cats, catKeyType); + } + if (cat == null) { + // This is a new category. Create it and add it to the list. + cat = mOpApi.createCategory(catKeyType, catKeyValue); + synchronized (cats) { + cats.add(cat); + } + } + return cat; + } + + private PkgCategory findCurrentCategory( + List> currentCategories, + CategoryKeyType catKeyType) { + for (PkgCategory cat : currentCategories) { + if (cat.getKeyType() == catKeyType) { + return cat; + } + } + return null; + } + + private PkgCategory findCurrentCategory( + List> currentCategories, + CategoryKeyType catKeyType, Object categoryKeyValue) { + for (PkgCategory cat : currentCategories) { + if ((cat.getKeyType() == catKeyType) && (cat.getKeyValue().equals(categoryKeyValue))) + return cat; + } + return null; + } + + private MetaPackage metaPackageFromPackage(RepoPackage repoPackage) + { + String name = PackageAnalyser.getNameFromPath(repoPackage.getPath()); + MetaPackage metaPackage = getMetaPackage(name); + return metaPackage != null ? metaPackage : getMetaPackage(PackageAnalyser.GENERIC); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java new file mode 100644 index 00000000..647dc451 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java @@ -0,0 +1,130 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import com.android.repository.api.RemotePackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.Archive; +import com.android.sdklib.AndroidVersion; + +public class PackageContentProvider implements ITreeContentProvider { + + private final IInputProvider mViewer; + private final PackageFilter mPackageFilter; + private boolean mDisplayArchives; + + public PackageContentProvider(IInputProvider viewer, PackageFilter packageFilter) + { + this.mViewer = viewer; + this.mPackageFilter = packageFilter; + } + + public void setDisplayArchives(boolean displayArchives) { + mDisplayArchives = displayArchives; + } + + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return ((ArrayList) parentElement).toArray(); + + } else if (parentElement instanceof PkgCategory) { + @SuppressWarnings("unchecked") + PkgCategory pkgCategory = (PkgCategory)parentElement; + if (mPackageFilter.isFilterOn()) + return mPackageFilter.getFilteredItems(pkgCategory.getChildren()).toArray(); + return pkgCategory.getChildren().toArray(); + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + + UpdatablePackage pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return new Object[] { pkg }; + } + + return ((PkgItem) parentElement).getArchives(); + } + + } else if (parentElement instanceof RemotePackage) { + if (mDisplayArchives) { + return new Archive[]{((RemotePackage) parentElement).getArchive()}; + } + + } + + return new Object[0]; + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + @SuppressWarnings("unchecked") + public Object getParent(Object element) { + // This operation is expensive, so we do the minimum + // and don't try to cover all cases. + + if (element instanceof PkgItem) { + Object input = mViewer.getInput(); + if (input != null) { + for (PkgCategory cat : (List>) input) { + if (cat.getItems().contains(element)) { + return cat; + } + } + } + } + + return null; + } + + + @Override + public boolean hasChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return true; + + } else if (parentElement instanceof PkgCategory) { + return true; + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + UpdatablePackage pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return true; + } + + Archive[] archives = ((PkgItem) parentElement).getArchives(); + return archives.length > 0; + } + } else if (parentElement instanceof RemotePackage) { + if (mDisplayArchives) { + return ((RemotePackage) parentElement).getArchive() != null; + } + } + + return false; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageFilter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageFilter.java new file mode 100644 index 00000000..18b9893e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageFilter.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import com.android.sdklib.AndroidVersion; + +/** + * Provides functions to select categories and and package items + * based on user provided set of package types + * @author Andrew Bowley + * + * 24-11-2017 + */ +public class PackageFilter { + + public static Set EMPTY_PACKAGE_TYPE_SET; + public static final PackageType[] GENERIC_PACKAGE_TYPES; + + static + { + EMPTY_PACKAGE_TYPE_SET = Collections.emptySet(); + GENERIC_PACKAGE_TYPES = new PackageType[] { + PackageType.emulator, + PackageType.cmake, + PackageType.docs, + PackageType.lldb, + PackageType.ndk_bundle, + PackageType.patcher, + PackageType.generic, + PackageType.samples + }; + } + + /** Set of package types on which to filter. An empty set indicates no filtering */ + private Set packageTypeSet; + private boolean selectTools; + private boolean selectApi; + private boolean selectExtra; + private boolean selectGeneric; + + /** + * Default constructor + */ + public PackageFilter() { + packageTypeSet = EMPTY_PACKAGE_TYPE_SET; + } + + /** + * Construct PackageFilter object with given selection set + */ + public PackageFilter(Set packageTypes) { + if ((packageTypes == null) || (packageTypes.isEmpty())) + packageTypeSet = EMPTY_PACKAGE_TYPE_SET; + else { + packageTypeSet = new TreeSet<>(); + packageTypeSet.addAll(packageTypes); + initialize(); + } + } + + public void setPackageTypes(Set packageTypeSet) { + if (this.packageTypeSet.isEmpty()) + this.packageTypeSet = new TreeSet<>(); + this.packageTypeSet.clear(); + this.packageTypeSet.addAll(packageTypeSet); + initialize(); + } + + public Set getPackageTypes() { + return Collections.unmodifiableSet(packageTypeSet); + } + + public boolean isFilterOn() { + return !packageTypeSet.isEmpty(); + } + + public List> getFilteredApiCategories(List> cats) { + if (!isFilterOn()) + return cats; + List> selectCategories = new ArrayList<>(); + for (PkgCategory cat: cats) { + CategoryKeyType catKeyType = cat.getKeyType(); + switch(catKeyType) { + case TOOLS: + case TOOLS_PREVIEW: + if (selectTools) + selectCategories.add(cat); + break; + case API: + if (selectApi) + selectCategories.add(cat); + break; + case EXTRA: + if (selectExtra) + selectCategories.add(cat); + break; + case GENERIC: + if (selectGeneric) + selectCategories.add(cat); + break; + default: + break; + } + } + return selectCategories; + } + + public List getFilteredItems(List items) + { + if (!isFilterOn() || ((items == null) || items.isEmpty() || !(items.get(0) instanceof PkgItem))) + return items; + List selectItems = new ArrayList<>(); + for (INode node: items) { + PkgItem packageItem = (PkgItem)node; + PackageType packageType = packageItem.getMetaPackage().getPackageType(); + if (packageTypeSet.contains(packageType)) + selectItems.add(packageItem); + if (selectGeneric) { + for (int i = 0; i < GENERIC_PACKAGE_TYPES.length; ++i) + if (GENERIC_PACKAGE_TYPES[i] == packageType) { + selectItems.add(packageItem); + break; + } + } + } + return selectItems; + + } + + private void initialize() + { + if (!isFilterOn()) + return; + selectTools = selectApi = selectExtra = selectGeneric = false; + selectTools = + packageTypeSet.contains(PackageType.build_tools) || + packageTypeSet.contains(PackageType.platform_tools) || + packageTypeSet.contains(PackageType.tools); + selectApi = + packageTypeSet.contains(PackageType.platforms) || + packageTypeSet.contains(PackageType.add_ons) || + packageTypeSet.contains(PackageType.system_images) || + packageTypeSet.contains(PackageType.sources); + selectExtra = packageTypeSet.contains(PackageType.extras); + + for (int i = 0; i < GENERIC_PACKAGE_TYPES.length; ++i) + if (packageTypeSet.contains(GENERIC_PACKAGE_TYPES[i])) { + selectGeneric = true; + break; + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageInstaller.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageInstaller.java new file mode 100644 index 00000000..96e590c8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageInstaller.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import com.android.repository.api.LocalPackage; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.util.InstallerUtil; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.PackageInstallListener; +import com.android.sdkuilib.internal.repository.PackageInstallTask; +import com.android.sdkuilib.internal.repository.ui.SdkProgressFactory; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterChooserDialog; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; + +/** + * Installs specified packages and their dependencies. + * The package details must first be loaded by PackageAnalyser. + * At the start of the installation, a dialog informs the user of the packages to + * be installed and prompts for acceptance of license terms. + * @author Andrew Bowley + * + * 29-11-2017 + */ +public class PackageInstaller { + + private static final String NONE_INSTALLED = "Done. Nothing was installed."; + + private final List requiredPackageItems = new ArrayList(); + private final List remotes = new ArrayList<>(); + private final Map updateMap = new HashMap<>(); + private final SdkProgressFactory factory; + private volatile int numInstalled; + + /** + * + */ + public PackageInstaller(PackageAnalyser packageAnalyser, PackageVisitor packageVisitor, SdkProgressFactory factory) { + this.factory = factory; + for (PkgItem packageItem: packageAnalyser.getAllPkgItems()) { + if (!packageVisitor.visit(packageItem)) + break; + } + for (PkgItem packageItem: packageAnalyser.getAllPkgItems()) { + // Is this the package we want to install? + if (packageVisitor.accept(packageItem)) + requiredPackageItems.add(packageItem); + } + assemblePackages(); + } + + /** + * + */ + public PackageInstaller(List requiredPackageItems, SdkProgressFactory factory) { + this.factory = factory; + this.requiredPackageItems.addAll(requiredPackageItems); + assemblePackages(); + } + + public Collection getRequiredPackageItems() { + return requiredPackageItems; + } + + public int getNumInstalled() { + return numInstalled; + } + + public void installPackages(Shell shell, SdkContext sdkContext, PackageInstallListener installListener) { + ITaskFactory taskFactory = factory; + List acceptedRemotes = new ArrayList<>(); + ITask prepareTask = new ITask(){ + + @Override + public void run(ITaskMonitor monitor) { + if (computeDependencies(sdkContext) > 0) { + SdkUpdaterChooserDialog dialog = + new SdkUpdaterChooserDialog(shell, sdkContext, updateMap.values(), remotes); + Display.getDefault().syncExec(new Runnable(){ + + @Override + public void run() { + dialog.open(); + acceptedRemotes.addAll(dialog.getResult()); + }}); + } else { + ILogUiProvider sdkProgressControl = factory.getProgressControl(); + sdkProgressControl.setDescription(NONE_INSTALLED); + if (installListener != null) { + installListener.onPackagesInstalled(0); + } + } + }}; + + taskFactory.start("Preparing Packages", prepareTask, new Runnable(){ + + @Override + public void run() { + if (acceptedRemotes.size() > 0) { + ILogUiProvider sdkProgressControl = factory.getProgressControl(); + PackageInstallTask packageInstallTask = new PackageInstallTask(sdkContext, remotes, acceptedRemotes) { + @Override + public void run() { + int count = getNumInstalled(); + numInstalled = count; + if (count == 0) { + sdkProgressControl.setDescription(NONE_INSTALLED); + } + else { + sdkProgressControl.setDescription(String.format("Done. %1$d %2$s installed.", + count, + count == 1 ? "package" : "packages")); + } + if (installListener != null) + installListener.onPackagesInstalled(count); + } + }; + factory.start("Installing Packages", packageInstallTask, packageInstallTask); + } else { + ILogUiProvider sdkProgressControl = factory.getProgressControl(); + sdkProgressControl.setDescription(NONE_INSTALLED); + if (installListener != null) { + installListener.onPackagesInstalled(0); + } + } + }}); + } + + private boolean assemblePackages() { + List installedList = null; + for (PkgItem item: requiredPackageItems) { + RemotePackage remotePackage = null; + if (item.hasUpdatePkg()) { // Update installed package + remotePackage = item.getUpdatePkg().getRemote(); + updateMap.put(remotePackage, item.getUpdatePkg()); + } else if (item.getMainPackage() instanceof LocalPackage) { + factory.getProgressControl().setDescription("Package " + item.getMainPackage().getDisplayName() + " is already installed on the local SDK"); + if (installedList == null) + installedList = new ArrayList<>(); + installedList.add(item); + continue; + } else { + remotePackage = (RemotePackage)item.getMainPackage(); + } + remotes.add(remotePackage); + } + if (installedList != null) + requiredPackageItems.removeAll(installedList); + return !requiredPackageItems.isEmpty(); + } + + private int computeDependencies(SdkContext sdkContext) { + // computeRequiredPackages() may produce a list containing duplicates! + ProgressIndicator progress = sdkContext.getProgressIndicator(); + List requiredPackages = InstallerUtil.computeRequiredPackages( + remotes, sdkContext.getPackages(), progress); + if (requiredPackages == null) { + progress.logWarning("Unable to compute a complete list of dependencies."); + return 0; + } + Iterator iterator = requiredPackages.iterator(); + Set existenceSet = new HashSet<>(); + existenceSet.addAll(remotes); + while (iterator.hasNext()) { + RemotePackage requiredPackage = iterator.next(); + if (!existenceSet.contains(requiredPackage)) { + { + existenceSet.add(requiredPackage); + remotes.add(requiredPackage); + } + } + } + // Remove references now existenceSet no longer needed + existenceSet.clear(); + return remotes.size(); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java new file mode 100644 index 00000000..0fb434bd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java @@ -0,0 +1,41 @@ +package com.android.sdkuilib.internal.repository.content; + +public enum PackageType { + tools, + platform_tools, + build_tools, + platforms, + add_ons, + system_images, + sources, + samples, + docs, + extras, + emulator, + cmake, + lldb, + ndk_bundle, + patcher, + generic; + + public String label; + + static { + tools.label = "Tools"; + platform_tools.label = "Platform tools"; + build_tools.label = "Build tools"; + platforms.label = "Platforms"; + add_ons.label = "Add ons"; + system_images.label = "System images"; + sources.label = "Sources"; + samples.label = "Samples"; + docs.label = "Documents"; + extras.label = "Extras"; + emulator.label = "Emulators"; + cmake.label = "cmake"; + lldb.label = "Layout Libraries"; + ndk_bundle.label = "NDK bundle"; + patcher.label = "Patcher"; + generic.label = "Generic"; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageVisitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageVisitor.java new file mode 100644 index 00000000..8163208e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageVisitor.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.content; + +/** + * Interface for active package filter which traverses the entire package collection, + * once to collect aggregate information by method visit() and then a second time, + * selecting packages by method accept(). + * @author Andrew Bowley + * + * 29-11-2017 + */ +public abstract class PackageVisitor { + + /** + * Visit a new package definition, in case we need to adjust the filter dynamically. + * @param pkg Package item + * @return boolean set true to continue traversal of packages + */ + public boolean visit(PkgItem pkg) { + return false; + } + + /** + * Checks whether this is the package meets criteria + * @param pkg Package item + * @return boolean set true if package is to be accepted + */ + public boolean accept(PkgItem pkg) { + return false; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java new file mode 100644 index 00000000..321ba0d6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java @@ -0,0 +1,172 @@ +/* + * 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.sdkuilib.internal.repository.content; + + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.ui.PackagesPage; + +/** + * PkgCategory represents the first level of the package tree + * @author Andrew Bowley + * + * 10-11-2017 + */ +public abstract class PkgCategory extends INode { + protected final CategoryKeyType keyType; + protected final K keyValue; + protected final String imageReference; + protected final List packageList = new ArrayList(); + protected final Map productMap = new TreeMap<>(); + protected String label; + protected boolean selectAllPackages = false; + + public PkgCategory(CategoryKeyType keyType, String label, String imageReference) { + this(keyType, null, label, imageReference); + } + + public PkgCategory(CategoryKeyType keyType, K keyValue, String label, String imageReference) { + super(); + this.keyType = keyType; + this.keyValue = keyValue; + this.label = label; + this.imageReference = imageReference; + } + + public CategoryKeyType getKeyType() + { + return keyType; + } + + public K getKeyValue() { + return keyValue; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setSelectAllPackages(boolean selectAllPackages) { + this.selectAllPackages = selectAllPackages; + } + + public List getItems() { + return packageList; + } + + public void clearProducts() { + productMap.clear(); + } + + public void putProduct(String product, PkgItem item) { + productMap.put(product, item); + } + + public PkgItem getProduct(String product) { + return productMap.get(product); + } + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element + */ + @Override + public String getText(Object element, int columnIndex) { + return columnIndex == PackagesPage.NAME ? getLabel() : VOID; + } + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element + */ + @Override + public String getImage(Object element, int columnIndex) { + if (columnIndex == PackagesPage.NAME) + return imageReference; + return VOID; + } + + /** + * Returns list of descendents + * @return INode list + */ + @Override + public List getChildren() { + if (!selectAllPackages) { + String product = null; + List filteredPackageList = new ArrayList(); + Iterator iterator = packageList.iterator(); + while (iterator.hasNext()) { + PkgItem packageItem = iterator.next(); + if ((packageItem.getState() != PkgState.NEW) || + !packageItem.getProduct().equals(product)) { + product = packageItem.getProduct(); + filteredPackageList.add(packageItem); + } + } + return filteredPackageList; + } + return packageList; + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + keyValue == null ? keyType.toString() : keyValue.toString(), + label, + packageList.size()); + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((keyValue == null) ? keyType.hashCode() : keyValue.hashCode()); + return result; + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PkgCategory other = (PkgCategory) obj; + if (keyType != other.keyType) + return false; + if (keyValue == null) { + if (other.keyValue != null) return false; + } else if (!keyValue.equals(other.keyValue)) return false; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java new file mode 100644 index 00000000..8964b25b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java @@ -0,0 +1,105 @@ +/* + * 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.sdkuilib.internal.repository.content; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkVersionInfo; + + +public class PkgCategoryApi extends PkgCategory { + + /** Platform name, in the form "Android 1.2". Can be null if we don't have the name. */ + private String mPlatformName; + + // When sorting by Source, key is the hash of the source's name. + // When storing by API, key is the AndroidVersion (API level >=1 + optional codename). + // We always want categories in order tools..platforms..extras; to achieve that tools + // and extras have the special values so they get "naturally" sorted the way we want + // them. + + public PkgCategoryApi(CategoryKeyType keyType, AndroidVersion version, String iconRef) { + super(keyType, version, null /*label*/, iconRef); + if (version != null) + setPlatformName(SdkVersionInfo.getVersionWithCodename(version)); + } + + public String getPlatformName() { + return mPlatformName; + } + + public void setPlatformName(String platformName) { + if (platformName != null) { + // Normal case for actual platform categories + mPlatformName = String.format("Android %1$s", platformName); + super.setLabel(null); + } + } + + public String getApiLabel() { + if (getKeyType() == CategoryKeyType.TOOLS) { + return "TOOLS"; //$NON-NLS-1$ + } else if (getKeyType() == CategoryKeyType.TOOLS_PREVIEW){ + return "TOOLS-PREVIEW"; //$NON-NLS-1$ + } else if (getKeyType() == CategoryKeyType.EXTRA) { + return "EXTRAS"; //$NON-NLS-1$ + } else if (getKeyType() == CategoryKeyType.GENERIC) { + return "GENERIC"; //$NON-NLS-1$ + } + AndroidVersion key = getKeyValue(); + return key.getApiString(); + } + + @Override + public String getLabel() { + String label = super.getLabel(); + if (label == null) { + CategoryKeyType key = getKeyType(); + + if (key == CategoryKeyType.TOOLS) { + label = "Tools"; + } else if (key == CategoryKeyType.TOOLS_PREVIEW) { + label = "Tools (Preview Channel)"; + } else if (key == CategoryKeyType.EXTRA) { + label = "Extras"; + } else if (key == CategoryKeyType.GENERIC) { + label = "Generic"; + } else { + if (mPlatformName != null) { + label = String.format("%1$s (%2$s)", mPlatformName, getApiLabel()); + } else { + label = getApiLabel(); + } + } + super.setLabel(label); + } + return label; + } + + @Override + public void setLabel(String label) { + throw new UnsupportedOperationException("Use setPlatformName() instead."); + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + getApiLabel(), + getLabel(), + getItems().size()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java new file mode 100644 index 00000000..1fadf298 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.sdkuilib.internal.repository.content; + +import java.util.List; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.graphics.Font; + +import com.android.sdklib.AndroidVersion; +import org.eclipse.andmore.base.resources.ImageFactory; + +public class PkgCellAgent { + + /** Column identities */ + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; + + private final Font mTreeFontItalic; + private final ImageFactory mImgFactory; + private final List> mCategoryList; + + public PkgCellAgent(SdkContext sdkContext, PackageAnalyser packageAnalyser, Font treeFontItalic) { + this.mTreeFontItalic = treeFontItalic; + this.mImgFactory = sdkContext.getSdkHelper().getImageFactory(); + this.mCategoryList = packageAnalyser.getApiCategories(); + } + + public Font getTreeFontItalic() + { + return mTreeFontItalic; + } + + public ImageFactory getImgFactory() { + return mImgFactory; + } + + public List> getCategoryList() { + return mCategoryList; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java new file mode 100644 index 00000000..dfe38289 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java @@ -0,0 +1,79 @@ +package com.android.sdkuilib.internal.repository.content; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ITableFontProvider; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +import com.android.sdkuilib.internal.repository.content.INode.LabelFont; +import org.eclipse.andmore.base.resources.ImageFactory; + +public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { + + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; + private final int columnIndex; + private final PkgCellAgent agent; + + public PkgCellLabelProvider(PkgCellAgent agent, int columnIndex) { + super(); + this.columnIndex = columnIndex; + this.agent = agent; + } + + @Override + public String getText(Object element) { + INode node = (INode)element; + String text = node.getText(element, columnIndex); + return text != INode.VOID ? text : null; + } + + /** + * The image is owned by the label provider and must not be disposed directly. + */ + @Override + public Image getImage(Object element) { + ImageFactory imgFactory = agent.getImgFactory(); + if (imgFactory != null) { + INode node = (INode)element; + String reference = node.getImage(element, columnIndex); + if (reference != INode.VOID) + return imgFactory.getImageByName(reference); + } + return super.getImage(element); + } + + // -- ITableFontProvider + + @Override + public Font getFont(Object element, int columnIndex) { + INode node = (INode)element; + LabelFont fontType = node.getFont(element, columnIndex); + if (fontType == LabelFont.italic) + return agent.getTreeFontItalic(); + return super.getFont(element); + } + + // -- Tooltip support + @Override + public String getToolTipText(Object element) { + INode node = (INode)element; + String text = node.getToolTipText(element); + if (text != INode.VOID) + return text; + return super.getToolTipText(element); + } + + @Override + public Point getToolTipShift(Object object) { + return new Point(15, 5); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + return 500; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java new file mode 100644 index 00000000..9dcd391a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java @@ -0,0 +1,389 @@ +/* + * 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.sdkuilib.internal.repository.content; + +import org.eclipse.andmore.sdktool.Utilities; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.repository.Revision; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.Archive; +import com.android.repository.impl.meta.TypeDetails; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.ui.PackagesPage; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; + +/** + * A {@link PkgItem} represents one main {@link Package} combined with its state + * and an optional update package. + *

+ * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ +public class PkgItem extends INode implements Comparable { + private static final String ICON_PKG_OBSOLETE = "error_icon_16.png"; + + private final MetaPackage metaPackage; + private PkgState state; + private RepoPackage mainPackage; + private UpdatablePackage updatePackage; + private String product; + private String version; + + /** + * Create a new {@link PkgItem} for this main package. + * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ + public PkgItem(RepoPackage mainPackage, MetaPackage metaPackage, PkgState state) { + super(); + this.mainPackage = mainPackage; + this.metaPackage = metaPackage; + this.state = state; + analysePath(); + } + + public String getProduct() { + return product; + } + + public String getVersion() { + return version; + } + + public boolean isObsolete() { + return mainPackage.obsolete(); + } + + public UpdatablePackage getUpdatePkg() { + return updatePackage; + } + + public void setUpdatePkg(UpdatablePackage updatePkg) { + updatePackage = updatePkg; + } + + public boolean hasUpdatePkg() { + return updatePackage != null; + } + + public String getName() { + return mainPackage.getDisplayName(); + } + + public Revision getRevision() { + return mainPackage.getVersion(); + } + + public MetaPackage getMetaPackage() + { + return metaPackage; + } + + public RepoPackage getMainPackage() { + return mainPackage; + } + + public PkgState getState() { + return state; + } + + @Nullable + public AndroidVersion getAndroidVersion() { + return getAndroidVersion(mainPackage); + } + + public Archive[] getArchives() { + if (state == PkgState.NEW) + return new Archive[]{((RemotePackage)mainPackage).getArchive()}; + return new Archive[0]; + } + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element + */ + @Override + public String getImage(Object element, int columnIndex) { + if (columnIndex == PackagesPage.NAME) + return metaPackage.getIconResource(); + else if (columnIndex == PackagesPage.STATUS) { + switch(state) { + case INSTALLED: + if (isObsolete()) + return ICON_PKG_OBSOLETE; + if (updatePackage != null) { + return PackagesPageIcons.ICON_PKG_UPDATE; + } else { + return PackagesPageIcons.ICON_PKG_INSTALLED; + } + case NEW: + if (isObsolete()) + return ICON_PKG_OBSOLETE; + return PackagesPageIcons.ICON_PKG_NEW; + case DELETED: + return PackagesPageIcons.ICON_PKG_INCOMPAT; + } + } + return VOID; + } + + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element, or VOID if there is no text label for the given object + */ + @Override + public String getText(Object element, int columnIndex) { + switch (columnIndex) + { + case PackagesPage.NAME: + return decorate(getPkgItemName()); + case PackagesPage.API: + { + AndroidVersion version = getAndroidVersion(); + return version == null ? VOID : getAndroidVersion().getApiString(); + } + case PackagesPage.REVISION: + // Do use version from path, if available to pick up alpha/beta + if (version != VOID) + return version; + return mainPackage.getVersion().toString(); + case PackagesPage.STATUS: + return getStatusText(); + default: + } + return VOID; + } + + private String decorate(String pkgItemName) { + if (isObsolete()) + return pkgItemName + "(obsolete)"; + return pkgItemName; + } + + /** + * Get the text displayed in the tool tip for given element + * @param element Target + * @return the tooltop text, or VOID for no text to display + */ + @Override + public String getToolTipText(Object element) { + String s = getTooltipDescription(mainPackage); + + if ((updatePackage != null) && updatePackage.isUpdate()) { + s += "\n-----------------" + //$NON-NLS-1$ + "\nUpdate Available:\n" + //$NON-NLS-1$ + getTooltipDescription(updatePackage.getRemote()); + } + return s; + } + + /** + * Mark item as checked according to given criteria. Force uncheck if no criteria specified. + * @param selectUpdates If true, select all update packages + * @param topApiLevel If greater than 0, select platform packages of this api level + */ + @Override + public void checkSelections( + boolean selectUpdates, + int topApiLevel) + { + boolean hasUpdate = (state == PkgState.INSTALLED) && (updatePackage != null); + if (selectUpdates && hasUpdate) { + setChecked(true); + return; + } + if (topApiLevel > 0) { + if (hasUpdate || // or new packages excluding system images and previews + ((state == PkgState.NEW) && (metaPackage.getPackageType() != PackageType.system_images))) { + AndroidVersion version = getAndroidVersion(); + if ((version != null) && (version.getApiLevel() == topApiLevel) && !version.isPreview()) { + setChecked(true); + return; + } + } + } else { + setChecked(false); + } + } + + @Override + public void markDeleted() + { + state = PkgState.DELETED; + setChecked(false); + updatePackage = null; + } + + @Override + public boolean isDeleted() + { + return state == PkgState.DELETED; + } + + /** Returns a string representation of this item, useful when debugging. */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('<'); + + if (isChecked) { + sb.append(" * "); //$NON-NLS-1$ + } + + sb.append(state.toString()); + + if (mainPackage != null) { + sb.append(", pkg:"); //$NON-NLS-1$ + sb.append(mainPackage.toString()); + } + + if (updatePackage != null) { + sb.append(", updated by:"); //$NON-NLS-1$ + sb.append(updatePackage.toString()); + } + + sb.append('>'); + return sb.toString(); + } + + @Override + public int compareTo(PkgItem other) { + if (other == null) + return Integer.MIN_VALUE; + int comparison1 = state.ordinal() - other.getState().ordinal(); + if (comparison1 != 0) + return comparison1; + if (hasUpdatePkg() && other.hasUpdatePkg()) + return updatePackage.compareTo(other.getUpdatePkg()); + return mainPackage.compareTo(other.getMainPackage()); + } + + /** + * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package + * and update package must be the similar. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof PkgItem) && (compareTo((PkgItem) obj) == 0); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + state.hashCode(); + result = prime * result + mainPackage.hashCode(); + result = prime * result + ((updatePackage == null) ? 0 : updatePackage.hashCode()); + return result; + } + + @Nullable + public static AndroidVersion getAndroidVersion(RepoPackage repoPackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getAndroidVersion(); + } + return null; + } + + public static boolean isPreview(RepoPackage repoPackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getCodename() != null; + } + return false; + } + + private void analysePath() { + // Parse package path to separate product from version + String path = mainPackage.getPath(); + int index = path.lastIndexOf(';'); + if (index != -1) { + String candidateVersion = path.substring(index + 1); + if (Character.isDigit(candidateVersion.charAt(0))) + version = candidateVersion; + else if (candidateVersion.startsWith("android-")) + version = candidateVersion.substring(8); + } + if (version == null) { + version = VOID; + product = path; + } else { + product = path.substring(0, index); + } + } + + + private String getStatusText() { + switch(state) { + case INSTALLED: + if (updatePackage != null) { + return String.format( + "Update available: rev. %1$s", + updatePackage.getRemote().getVersion().toString()); + } + return "Installed"; + + case NEW: + if (((RemotePackage)mainPackage).getArchive().isCompatible()) { + return "Not installed"; + } else { + return String.format("Not compatible with %1$s", + SdkConstants.currentPlatformName()); + } + case DELETED: + return "Deleted"; + } + return state.toString(); + } + + private String getTooltipDescription(RepoPackage repoPackage) { + String s = repoPackage.getDisplayName(); + if (repoPackage instanceof RemotePackage) { + RemotePackage remote = (RemotePackage) repoPackage; + // For non-installed item get download size + long fileSize = remote.getArchive().getComplete().getSize(); + s += '\n' + Utilities.formatFileSize(fileSize); + s += String.format("\nProvided by %1$s", remote.getSource().getUrl()); + } + return s; + } + + private String getPkgItemName() { + if (metaPackage.getPackageType() == PackageType.platforms) + return "Platform SDK"; + String displayName = mainPackage.getDisplayName(); + if (version != VOID) { + int index = displayName.indexOf(version); + if (index != -1) + return displayName.substring(0, index); + } + return displayName; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java new file mode 100644 index 00000000..30791d88 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java @@ -0,0 +1,137 @@ +/* + * 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.sdkuilib.internal.repository.content; + +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +/** + * A custom version of {@link TreeColumnViewerLabelProvider} which + * handles {@link TreePath}s and delegates content to the given + * {@link ColumnLabelProvider} for a given {@link TreeViewerColumn}. + *

+ * The implementation handles a variety of providers (table label, table + * color, table font) but does not implement a tooltip provider, so we + * delegate the calls here to the appropriate {@link ColumnLabelProvider}. + *

+ * Only {@link #getToolTipText(Object)} is really useful for us but we + * delegate all the tooltip calls for completeness and avoid surprises later + * if we ever decide to override more things in the label provider. + */ +public class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider { + private CellLabelProvider mTooltipProvider; + + public PkgTreeColumnViewerLabelProvider(ColumnLabelProvider columnLabelProvider) { + super(columnLabelProvider); + } + + @Override + public void setProviders(Object provider) { + super.setProviders(provider); + if (provider instanceof CellLabelProvider) { + mTooltipProvider = (CellLabelProvider) provider; + } + } + + @Override + public Image getToolTipImage(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipImage(object); + } + return super.getToolTipImage(object); + } + + @Override + public String getToolTipText(Object element) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipText(element); + } + return super.getToolTipText(element); + } + + @Override + public Color getToolTipBackgroundColor(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipBackgroundColor(object); + } + return super.getToolTipBackgroundColor(object); + } + + @Override + public Color getToolTipForegroundColor(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipForegroundColor(object); + } + return super.getToolTipForegroundColor(object); + } + + @Override + public Font getToolTipFont(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipFont(object); + } + return super.getToolTipFont(object); + } + + @Override + public Point getToolTipShift(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipShift(object); + } + return super.getToolTipShift(object); + } + + @Override + public boolean useNativeToolTip(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.useNativeToolTip(object); + } + return super.useNativeToolTip(object); + } + + @Override + public int getToolTipTimeDisplayed(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipTimeDisplayed(object); + } + return super.getToolTipTimeDisplayed(object); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipDisplayDelayTime(object); + } + return super.getToolTipDisplayDelayTime(object); + } + + @Override + public int getToolTipStyle(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipStyle(object); + } + return super.getToolTipStyle(object); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java new file mode 100644 index 00000000..78ae3d4e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java @@ -0,0 +1,37 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.List; + +import com.android.repository.api.RepoPackage; + +/** + * An update operation, customized to either sort by API or sort by source. + */ +public abstract class UpdateOp { + static public class SdkSource{} + + private final List> mCategories = new ArrayList<>(); + + /** Removes all internal state. */ + public void clear() { + } + + /** Retrieve the sorted category list. */ + public List> getCategories() { + return mCategories; + } + + /** Retrieve the category key type for the given package */ + public abstract CategoryKeyType getCategoryKeyType(RepoPackage pkg); + + /** Retrieve the category key value for the given package. May be null. */ + public abstract K getCategoryKeyValue(RepoPackage pkg); + + /** Creates the category for the given key and returns it. */ + public abstract PkgCategory createCategory(CategoryKeyType catKeyType, K catKeyValue); + + /** Sorts the category list (but not the items within the categories.) */ + public abstract void sortCategoryList(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java new file mode 100644 index 00000000..d4a59e5f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java @@ -0,0 +1,114 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; + +import com.android.repository.api.RepoPackage; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; +import com.android.sdklib.AndroidVersion; + +public class UpdateOpApi extends UpdateOp { + + @Override + public CategoryKeyType getCategoryKeyType(RepoPackage pkg) { + // Sort by API + AndroidVersion androidVersion = PkgItem.getAndroidVersion(pkg); + if (androidVersion != null) { + return CategoryKeyType.API; + + } else if (pkg.getPath().indexOf("tools") != -1) { + if (PkgItem.isPreview(pkg)) { + return CategoryKeyType.TOOLS_PREVIEW; + } else { + return CategoryKeyType.TOOLS; + } + } else if (pkg.getPath().indexOf("extras") != -1) { + return CategoryKeyType.EXTRA; + } else { + return CategoryKeyType.GENERIC; + } + } + + @Override + public AndroidVersion getCategoryKeyValue(RepoPackage pkg) { + // Sort by API + return PkgItem.getAndroidVersion(pkg); + } + + @Override + public PkgCategory createCategory(CategoryKeyType catKeyType, AndroidVersion catKeyValue) { + // Create API category. + PkgCategory cat = null; + + // We should not be trying to recreate the tools or extra categories. + //assert (catKeyType != CategoryKeyType.TOOLS) && (catKeyType != CategoryKeyType.EXTRA); + cat = new PkgCategoryApi( + catKeyType, + catKeyValue, + PackagesPageIcons.ICON_CAT_PLATFORM); + + return cat; + } + + @Override + public void sortCategoryList() { + // Sort the categories list. + // We always want categories in order tools..platforms..extras. + // For platform, we compare in descending order (o2-o1). + // This order is achieved by having the category keys ordered as + // needed for the sort to just do what we expect. + + synchronized (getCategories()) { + Collections.sort(getCategories(), new Comparator>() { + @Override + public int compare(PkgCategory cat1, PkgCategory cat2) { + int comparison1 = cat1.getKeyType().ordinal() - cat2.getKeyType().ordinal(); + if ((cat1.getKeyType() == CategoryKeyType.API) && (cat2.getKeyType() == CategoryKeyType.API)) + return cat2.getKeyValue().compareTo(cat1.getKeyValue()); + else + return comparison1; + } + }); + for (PkgCategory cat: getCategories()) + sortPackages(cat); + } + } + + private void sortPackages(PkgCategory cat) + { + synchronized (cat) + { + Collections.sort(cat.getItems(), new Comparator() { + + @Override + public int compare(PkgItem item1, PkgItem item2) { + int ordinal1 = item1.getMetaPackage().getPackageType().ordinal(); + int ordinal2 = item2.getMetaPackage().getPackageType().ordinal(); + int comparison1 = ordinal1 - ordinal2; + if (comparison1 != 0) + return comparison1; + String name1 = PackageAnalyser.getNameFromPath(item1.getMainPackage().getPath()); + String name2 = PackageAnalyser.getNameFromPath(item2.getMainPackage().getPath()); + int comparison2 = name1.compareTo(name2); + // Use reverse lexical order of paths for same package types to get top down version ordering + return comparison2 != 0 ? comparison2 : item2.getMainPackage().getPath().compareTo(item1.getMainPackage().getPath()); + } + + }); + } + // Generate product map to support filtering on latest version + String product = null; + cat.clearProducts(); + Iterator iterator = cat.getItems().iterator(); + while (iterator.hasNext()) { + PkgItem packageItem = iterator.next(); + if ((packageItem.getState() == PkgState.NEW) && + !packageItem.getProduct().equals(product)) { + product = packageItem.getProduct(); + cat.putProduct(product, packageItem); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java new file mode 100644 index 00000000..f31e02f0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java @@ -0,0 +1,158 @@ +/* + * 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.sdkuilib.internal.repository.ui; + +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.sdkuilib.ui.AvdDisplayMode; +import com.android.sdkuilib.internal.widgets.AvdSelector; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +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.Label; + +/** + * An Update page displaying AVD Manager entries. + * This is the sole page displayed by {@link AvdManagerWindowImpl1}. + * + * Note: historically the SDK Manager was a single window with several sub-pages and a tab + * switcher. For simplicity each page was separated in its own window. The AVD Manager is + * thus composed of the {@link AvdManagerWindowImpl1} (the window shell itself) and this + * page displays the actually list of AVDs and various action buttons. + */ +public class AvdManagerPage extends Composite + implements ISdkChangeListener, DevicesChangedListener, DisposeListener { + + private AvdSelector mAvdSelector; + + private final SdkContext mSdkContext; + private final DeviceManager mDeviceManager; + /** + * Create the composite. + * @param parent The parent of the composite. + * @param sdkContext SDK handler and repo manager + */ + public AvdManagerPage(Composite parent, + int swtStyle, + SdkContext sdkContext) { + super(parent, swtStyle); + + mSdkContext = sdkContext; + mSdkContext.getSdkHelper().addListeners(this); + + mDeviceManager = mSdkContext.getDeviceManager(); + mDeviceManager.registerListener(this); + + createContents(this); + postCreate(); //$hide$ + } + + private void createContents(Composite parent) { + parent.setLayout(new GridLayout(1, false)); + + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData()); + + try { + label.setText(String.format( + "List of existing Android Virtual Devices located at %s", + mSdkContext.getAvdManager().getBaseAvdFolder())); + } catch (AndroidLocationException e) { + label.setText(e.getMessage()); + } + + mAvdSelector = new AvdSelector(parent, + mSdkContext, + AvdDisplayMode.MANAGER); + } + + @Override + public void widgetDisposed(DisposeEvent e) { + dispose(); + } + + @Override + public void dispose() { + mSdkContext.getSdkHelper().removeListener(this); + mDeviceManager.unregisterListener(this); + super.dispose(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + public void selectAvd(AvdInfo avd, boolean reloadAvdList) { + if (reloadAvdList) { + mAvdSelector.refresh(true /*reload*/); + + // Reloading the AVDs created new objects, so the reference to avdInfo + // will never be selected. Instead reselect it based on its unique name. + AvdManager avdManager = mSdkContext.getAvdManager(); + avd = avdManager.getAvd(avd.getName(), false /*validAvdOnly*/); + } + mAvdSelector.setSelection(avd); + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** + * Called by the constructor right after {@link #createContents(Composite)}. + */ + private void postCreate() { + // nothing to be done for now. + } + + // --- Implementation of ISdkChangeListener --- + + @Override + public void onSdkReload() { + mAvdSelector.refresh(false /*reload*/); + } + + @Override + public void preInstallHook() { + // nothing to be done for now. + } + + @Override + public void postInstallHook() { + // nothing to be done for now. + } + + // --- Implementation of DevicesChangeListener + + @Override + public void onDevicesChanged() { + mAvdSelector.refresh(false /*reload*/); + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java new file mode 100644 index 00000000..47092743 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java @@ -0,0 +1,371 @@ +/* + * 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.sdkuilib.internal.repository.ui; + + +import com.android.SdkConstants; +import com.android.sdklib.internal.avd.AvdInfo; + +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.AboutDialog; +import com.android.sdkuilib.internal.repository.MenuBarWrapper; +import com.android.sdkuilib.internal.repository.SettingsDialog; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.repository.ui.DeviceManagerPage.IAvdCreatedListener; +import com.android.sdkuilib.repository.SdkUpdaterWindow; + +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.utils.ILogger; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; + +/** + * This is an intermediate version of the {@link AvdManagerPage} + * wrapped in its own standalone window for use from the SDK Manager 2. + */ +public class AvdManagerWindowImpl1 { + + private static final String APP_NAME = "Android Virtual Device (AVD) Manager"; + private static final String APP_NAME_MAC_MENU = "AVD Manager"; + private static final String SIZE_POS_PREFIX = "avdman1"; //$NON-NLS-1$ + + private final Shell mParentShell; + private final AvdInvocationContext mContext; + private final SdkContext mSdkContext; + + // --- UI members --- + + protected Shell mShell; + private AvdManagerPage mAvdPage; + private TabFolder mTabFolder; + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param sdkContext SDK handler and repo manager + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindowImpl1( + Shell parentShell, + ILogger sdkLog, + SdkContext sdkContext, + AvdInvocationContext context) { + mParentShell = parentShell; + mSdkContext = sdkContext; + mContext = context; + } + + /** + * Creates a new window. Caller must call open(), which will block. + *

+ * This is to be used when the window is opened from {@link SdkUpdaterWindowImpl2} + * to share the same {@link SwtUpdaterData} structure. + * + * @param parentShell Parent shell. + * @param sdkContext SDK handler and repo manager + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindowImpl1( + Shell parentShell, + SdkContext sdkContext, + AvdInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSdkContext = sdkContext; + } + + /** + * Opens the window. + * @wbp.parser.entryPoint + */ + public void open() { + if (mParentShell == null) { + Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer) + } + + createShell(); + preCreateContent(); + createContents(); + createMenuBar(); + mShell.open(); + mShell.layout(); + + boolean ok = postCreateContent(); + + if (ok && mContext == AvdInvocationContext.STANDALONE) { + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + dispose(); //$hide$ + } + } + + private void createShell() { + // The AVD Manager must use a shell trim when standalone + // or a dialog trim when invoked from somewhere else. + int style = SWT.SHELL_TRIM; + if (mContext != AvdInvocationContext.STANDALONE) { + style |= SWT.APPLICATION_MODAL; + } + + mShell = new Shell(mParentShell, style); + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX); //$hide$ + mAvdPage.dispose(); //$hide$ + } + }); + + GridLayout glShell = new GridLayout(2, false); + glShell.verticalSpacing = 0; + glShell.horizontalSpacing = 0; + glShell.marginWidth = 0; + glShell.marginHeight = 0; + mShell.setLayout(glShell); + + mShell.setMinimumSize(new Point(600, 300)); + mShell.setSize(700, 500); + mShell.setText(APP_NAME); + + ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX); + } + + private void createContents() { + mTabFolder = new TabFolder(mShell, SWT.NONE); + GridDataBuilder.create(mTabFolder).fill().grab().hSpan(2); + + // avd tab + TabItem avdTabItem = new TabItem(mTabFolder, SWT.NONE); + avdTabItem.setText("Android Virtual Devices"); + avdTabItem.setToolTipText(avdTabItem.getText()); + createAvdTab(mTabFolder, avdTabItem); + + // device tab + TabItem devTabItem = new TabItem(mTabFolder, SWT.NONE); + devTabItem.setText("Device Definitions"); + devTabItem.setToolTipText(devTabItem.getText()); + createDeviceTab(mTabFolder, devTabItem); + } + + private void createAvdTab(TabFolder tabFolder, TabItem avdTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + avdTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(1); + + mAvdPage = new AvdManagerPage(root, SWT.NONE, mSdkContext); + GridDataBuilder.create(mAvdPage).fill().grab(); + } + + private void createDeviceTab(TabFolder tabFolder, TabItem devTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + devTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(1); + + DeviceManagerPage devicePage = + new DeviceManagerPage(root, SWT.NONE, mSdkContext, new SdkTargets(mSdkContext)); + GridDataBuilder.create(devicePage).fill().grab(); + + devicePage.setAvdCreatedListener(new IAvdCreatedListener() { + @Override + public void onAvdCreated(AvdInfo avdInfo) { + if (avdInfo != null) { + mTabFolder.setSelection(0); // display mAvdPage + mAvdPage.selectAvd(avdInfo, true /*reloadAvdList*/); + } + } + }); + } + + // MenuBarWrapper works using side effects + private void createMenuBar() { + Menu menuBar = new Menu(mShell, SWT.BAR); + mShell.setMenuBar(menuBar); + + // Only create the tools menu when running as standalone. + // We don't need the tools menu when invoked from the IDE, or the SDK Manager + // or from the AVD Chooser dialog. The only point of the tools menu is to + // get the about box, and invoke Tools > SDK Manager, which we don't + // need to do in these cases. + if (mContext == AvdInvocationContext.STANDALONE) { + + MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE); + menuBarTools.setText("Tools"); + + Menu menuTools = new Menu(menuBarTools); + menuBarTools.setMenu(menuTools); + + MenuItem manageSdk = new MenuItem(menuTools, SWT.NONE); + manageSdk.setText("Manage SDK..."); + manageSdk.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + onSdkManager(); + } + }); + + try { + new MenuBarWrapper(APP_NAME_MAC_MENU, menuTools) { + @Override + public void onPreferencesMenuSelected() { + SettingsDialog sd = new SettingsDialog(mShell, mSdkContext); + sd.open(); + } + + @Override + public void onAboutMenuSelected() { + AboutDialog ad = new AboutDialog(mShell, mSdkContext); + ad.open(); + } + + @Override + public void printError(String format, Object... args) { + if (mSdkContext != null) { + mSdkContext.getSdkLog().error(null, format, args); + } + } + }; + } catch (Throwable e) { + mSdkContext.getSdkLog().error(e, "Failed to setup menu bar"); + e.printStackTrace(); + } + } + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // --- Public API ----------- + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + */ + public void addListener(ISdkChangeListener listener) { + mSdkContext.getSdkHelper().addListeners(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public void removeListener(ISdkChangeListener listener) { + mSdkContext.getSdkHelper().removeListener(listener); + } + + // --- Internals & UI Callbacks ----------- + + /** + * Called before the UI is created. + */ + private void preCreateContent() { + mSdkContext.getSdkHelper().setWindowShell(mShell); + // Note: we can't create the TaskFactory yet because we need the UI + // to be created first, so this is done in postCreateContent(). + } + + /** + * Once the UI has been created, initializes the content. + * This creates the pages, selects the first one, setup sources and scan for local folders. + * + * Returns true if we should show the window. + */ + private boolean postCreateContent() { + setWindowImage(mShell); + + if (!initializeSettings()) { + return false; + } + // TODO - Consider how to signal SDK loaded + //mSdkContext.getSdkHelper().broadcastOnSdkLoaded(mSdkContext.getSdkLog()); + return true; + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; + } + + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Called by the main loop when the window has been disposed. + */ + private void dispose() { + //mSdkContext.getSources().saveUserAddons(mSdkContext.getSdkLog()); + } + + /** + * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. + */ + private boolean initializeSettings() { + return mSdkContext.getSettings().initialize(mSdkContext.getSdkLog()); + } + + private void onSdkManager() { + try { + SdkUpdaterWindowImpl2 win = new SdkUpdaterWindowImpl2( + mShell, + mSdkContext, + SdkUpdaterWindow.SdkInvocationContext.AVD_MANAGER); + + win.open(); + } catch (Exception e) { + mSdkContext.getSdkLog().error(e, "SDK Manager window error"); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java new file mode 100644 index 00000000..261a571b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java @@ -0,0 +1,830 @@ +/* + * 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.sdkuilib.internal.repository.ui; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DeviceFilter; +import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.Storage; +import com.android.sdklib.devices.Storage.Unit; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.ImageFactory.ImageEditor; +import com.android.sdkuilib.internal.widgets.AvdCreationDialog; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.DeviceCreationDialog; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.window.Window; +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.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Resource; +import org.eclipse.swt.graphics.TextLayout; +import org.eclipse.swt.graphics.TextStyle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +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.Shell; +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.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A page displaying Device Manager entries. + *

+ * This is displayed as a second tab in the AVD Manager window. + * The layout purposely matches the one from {@link AvdManagerPage} and {@link AvdSelector} + * so that there's a good consistency when switching tabs. + * The table displays a few properties of each device as well as actions to edit/add/delete + * devices and a button to create an AVD from a given device. + * + * Non-goals: this tries to keep it simple for a first iteration. Possible enhancements: + * - a way to sort the device list by name, manufacturer or screen size. + * - possibly a tree organized by manufacturer. + * - a filter box to do a string search on any part of the display. + */ +public class DeviceManagerPage extends Composite + implements ISdkChangeListener, DevicesChangedListener, DisposeListener { + + public interface IAvdCreatedListener { + public void onAvdCreated(AvdInfo createdAvdInfo); + } + + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; + private final DeviceManager mDeviceManager; + private Table mTable; + private Button mNewButton; + private Button mEditButton; + private Button mDeleteButton; + private Button mNewAvdButton; + private Button mRefreshButton; + private ImageFactory mImageFactory; + private Image mUserImage; + private Image mDeviceImage; + private int mImageWidth; + private boolean mDisableRefresh; + private IAvdCreatedListener mAvdCreatedListener; + private final ImageEditor mUserColorFilter = new ImageEditor() { + @Override + public ImageData edit(Image source) { + ImageData srcData = source.getImageData(); + + // swap green and blue + PaletteData p = srcData.palette; + int b = p.blueMask; + p.blueMask = p.greenMask; + p.greenMask = b; + + b = p.blueShift; + p.blueShift = p.greenShift; + p.greenShift = b; + + return srcData; + } + }; + + /** + * Create the composite. + * @param parent The parent of the composite. + * @param SdkContext An instance of {@link SdkContext}. + */ + public DeviceManagerPage(Composite parent, + int swtStyle, + SdkContext sdkContext, + SdkTargets sdkTargets) { + super(parent, swtStyle); + + mSdkContext = sdkContext; + mSdkTargets = sdkTargets; + mSdkContext.getSdkHelper().addListeners(this); + + mDeviceManager = mSdkContext.getDeviceManager(); + mDeviceManager.registerListener(this); + + createContents(this); + postCreate(); //$hide$ + } + + public void setAvdCreatedListener(IAvdCreatedListener avdCreatedListener) { + mAvdCreatedListener = avdCreatedListener; + } + + private void createContents(Composite parent) { + + // get some bitmaps. + mImageFactory = mSdkContext.getSdkHelper().getImageFactory(); + mUserImage = getTagImage(null /*tag*/, true /*isUser*/); + mDeviceImage = getTagImage(null /*tag*/, false /*isUser*/); + mImageWidth = Math.max(mDeviceImage.getImageData().width, + Math.max(mUserImage.getImageData().width, + mDeviceImage.getImageData().width)); + + // Layout has 2 columns + GridLayoutBuilder.create(parent).columns(2); + + // Insert a top label explanation. This matches the design in AvdManagerPage so + // that the table starts at the same height on both tabs. + Label label = new Label(parent, SWT.NONE); + label.setText("List of known device definitions. This can later be used to create Android Virtual Devices."); + GridDataBuilder.create(label).hSpan(2); + + // Device table. + mTable = new Table(parent, SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(true); + mTable.setFont(parent.getFont()); + setTableHeightHint(30); + + // Buttons on the side. + Composite buttons = new Composite(parent, SWT.NONE); + GridLayoutBuilder.create(buttons).columns(1).noMargins(); + GridDataBuilder.create(buttons).vFill(); + buttons.setFont(parent.getFont()); + + mNewAvdButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewAvdButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewAvdButton.setText("Create AVD..."); + mNewAvdButton.setToolTipText("Creates a new AVD based on this device definition."); + mNewAvdButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onCreateAvd(); + } + }); + + @SuppressWarnings("unused") + Label spacing = new Label(buttons, SWT.NONE); + + mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewButton.setText("Create Device..."); + mNewButton.setToolTipText("Creates a new user device definition."); + mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onNewDevice(); + } + }); + + mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mEditButton.setText("Edit..."); + mEditButton.setToolTipText("Edit an existing device definition."); + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onEditDevice(); + } + }); + + mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDeleteButton.setText("Delete..."); + mDeleteButton.setToolTipText("Deletes the selected AVD."); + mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDeleteDevice(); + } + }); + + Composite padding = new Composite(buttons, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRefreshButton.setText("Refresh"); + mRefreshButton.setToolTipText("Reloads the list of devices."); + mRefreshButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onRefresh(); + } + }); + + // Legend at the bottom. + // This matches the one on AvdSelector so that the table height in the tab be similar. + Composite legend = new Composite(parent, SWT.NONE); + GridLayoutBuilder.create(legend).columns(4).noMargins(); + GridDataBuilder.create(legend).hFill().vTop().hGrab().hSpan(2); + legend.setFont(parent.getFont()); + + new Label(legend, SWT.NONE).setImage(mUserImage); + new Label(legend, SWT.NONE).setText("A user-created device definition."); + new Label(legend, SWT.NONE).setImage(mDeviceImage); + new Label(legend, SWT.NONE).setText("A generic device definition."); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("Device"); + + adjustColumnsWidth(mTable, column0); + setupSelectionListener(mTable); + fillTable(mTable); + updateButtonStates(); + setEnabled(true); + } + + private void adjustColumnsWidth(final Table table, final TableColumn column0) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 100 / 100 - 1); // 100% + } + }); + } + + private void setupSelectionListener(Table table) { + // TODO Auto-generated method stub + + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + if (heightHint > 0) { + data.heightHint = heightHint; + } + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + @Override + public void widgetDisposed(DisposeEvent e) { + dispose(); + } + + @Override + public void dispose() { + mSdkContext.getSdkHelper().removeListener(this); + mDeviceManager.unregisterListener(this); + super.dispose(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** + * Called by the constructor right after {@link #createContents(Composite)}. + */ + private void postCreate() { + // nothing to be done for now. + } + + + // ------- + + private static class CellInfo { + final boolean mIsUser; + final Device mDevice; + final TextLayout mWidget; + Rectangle mBounds; + + CellInfo(boolean isUser, Device device, TextLayout widget) { + mIsUser = isUser; + mDevice = device; + mWidget = widget; + } + } + + private void fillTable(final Table table) { + + table.removeAll(); + disposeTableResources(table.getData("disposeResources")); + + final List disposables = new ArrayList(); + + Font boldFont = getBoldFont(table); + if (boldFont != null) { + disposables.add(boldFont); + } else { + boldFont = table.getFont(); + } + + try { + mDisableRefresh = true; + disposables.addAll(fillDevices(table, boldFont, true, + mDeviceManager.getDevices(DeviceFilter.USER))); + disposables.addAll(fillDevices(table, boldFont, false, + mDeviceManager.getDevices(EnumSet.of(DeviceFilter.DEFAULT, + DeviceFilter.VENDOR, + DeviceFilter.SYSTEM_IMAGES)))); + } finally { + mDisableRefresh = false; + } + + table.setData("disposeResources", disposables); + + if (!Boolean.TRUE.equals(table.getData("createdTableListeners"))) { + table.addListener(SWT.PaintItem, new Listener() { + @Override + public void handleEvent(Event event) { + if (event.item != null) { + Object info = event.item.getData(); + if (info instanceof CellInfo) { + ((CellInfo) info).mWidget.draw(event.gc, event.x, event.y + 1); + } + } + } + }); + + table.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + if (event.item != null) { + Object info = event.item.getData(); + if (info instanceof CellInfo) { + CellInfo ci = (CellInfo) info; + Rectangle bounds = ci.mBounds; + if (bounds == null) { + // TextLayout.getBounds() seems expensive, so let's cache it. + ci.mBounds = bounds = ci.mWidget.getBounds(); + } + event.width = bounds.width + 2; + event.height = bounds.height + 4; + } + } + } + }); + + table.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent event) { + disposeTableResources(table.getData("disposeResources")); + } + }); + + table.addSelectionListener(new SelectionListener() { + /** Handles single clicks on a row. */ + @Override + public void widgetSelected(SelectionEvent event) { + updateButtonStates(); + } + + /** Handles double click on a row. */ + @Override + public void widgetDefaultSelected(SelectionEvent event) { + // FIXME: should double-click be to edit a device or create a new AVD? + onEditDevice(); + } + }); + } + + if (table.getItemCount() == 0) { + table.setEnabled(true); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "No devices available"); + return; + } + + table.setData("createdTableListeners", Boolean.TRUE); + } + + private void disposeTableResources(Object disposablesList) { + if (disposablesList instanceof List) { + for (Object obj : (List) disposablesList) { + if (obj instanceof Resource) { + ((Resource) obj).dispose(); + } + } + } + } + + private Font getBoldFont(Table table) { + Display display = table.getDisplay(); + FontData[] fds = table.getFont().getFontData(); + if (fds != null && fds.length > 0) { + fds[0].setStyle(SWT.BOLD); + return new Font(display, fds[0]); + } + return null; + } + + private List fillDevices( + Table table, + Font boldFont, + boolean isUser, + Collection devicesCollection) { + List disposables = new ArrayList(); + Display display = table.getDisplay(); + + TextStyle boldStyle = new TextStyle(); + boldStyle.font = boldFont; + + // We need the list to be be modifiable so that we can sort it. + ArrayList devices = new ArrayList(devicesCollection); + Collections.sort(devices, Device.getDisplayComparator()); + + // Generate a list of the AVD names using these devices + Map> device2avdMap = new HashMap>(); + for (AvdInfo avd : mSdkContext.getAvdManager().getAllAvds()) { + String n = avd.getDeviceName(); + String m = avd.getDeviceManufacturer(); + if (n == null || m == null || n.isEmpty() || m.isEmpty()) { + continue; + } + for (Device device : devices) { + if (m.equals(device.getManufacturer()) && n.equals(device.getDisplayName())) { + List list = device2avdMap.get(device); + if (list == null) { + list = new LinkedList(); + device2avdMap.put(device, list); + } + list.add(avd.getName()); + } + } + } + + final String prefix = "\n "; + + for (Device device : devices) { + TableItem item = new TableItem(table, SWT.NONE); + TextLayout widget = new TextLayout(display); + CellInfo ci = new CellInfo(isUser, device, widget); + item.setData(ci); + + widget.setIndent(mImageWidth * 2); + widget.setFont(table.getFont()); + + StringBuilder sb = new StringBuilder(); + String name = getPrettyName(device, false /*leadZeroes*/); + sb.append(name); + int pos1 = sb.length(); + + String manufacturer = device.getManufacturer(); + if (!manufacturer.contains(NEXUS)) { + sb.append(" by ").append(manufacturer); + } + + Image img = getTagImage(device.getTagId(), isUser); + item.setImage(img != null ? img : mDeviceImage); + + Hardware hw = device.getDefaultHardware(); + Screen screen = hw.getScreen(); + sb.append(prefix); + sb.append(String.format(java.util.Locale.US, + "Screen: %1$.1f\", %2$d \u00D7 %3$d, %4$s %5$s", // U+00D7: Unicode multiplication sign + screen.getDiagonalLength(), + screen.getXDimension(), + screen.getYDimension(), + screen.getSize().getShortDisplayValue(), + screen.getPixelDensity().getResourceValue() + )); + + Storage sto = hw.getRam(); + Unit unit = sto.getSizeAsUnit(Unit.GiB) > 1 ? Unit.GiB : Unit.MiB; + sb.append(prefix); + sb.append(String.format(java.util.Locale.US, + "RAM: %1$d %2$s", + sto.getSizeAsUnit(unit), + unit)); + + List avdList = device2avdMap.get(device); + if (avdList != null && !avdList.isEmpty()) { + sb.append(prefix); + sb.append("Used by: "); + boolean first = true; + for (String avd : avdList) { + if (!first) { + sb.append(", "); + } + sb.append(avd); + first = false; + } + } + + widget.setText(sb.toString()); + widget.setStyle(boldStyle, 0, pos1); + } + + return disposables; + } + + @Nullable + private Image getTagImage(@NonNull String tagId, boolean isUser) { + if (tagId == null) { + tagId = SystemImage.DEFAULT_TAG.getId(); + } + + String fname = String.format("tag_%s_32.png", tagId); + String kname = (isUser ? "user_" : "dev_") + fname; + return mImageFactory.getImageByName(fname, kname, isUser ? mUserColorFilter : null); + } + + // Constants extracted from DeviceMenuListerner -- TODO refactor somewhere else. + private static final String NEXUS = "Nexus"; //$NON-NLS-1$ + private static Pattern PATTERN = Pattern.compile( + "(\\d+\\.?\\d*)(?:in|\") (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$ + /** + * Returns a pretty name for the device. + * + * Extracted from DeviceMenuListener. + * Modified to remove the leading space insertion as it doesn't render + * neatly in the avd manager. Instead added the option to add leading + * zeroes to make the string names sort properly. + * + * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA) + * Use the same precision for all devices (all but one specify decimals) + * Add in screen resolution and density + */ + private static String getPrettyName(Device d, boolean leadZeroes) { + if (d == null) { + return ""; + } + String name = d.getDisplayName(); + if (name.equals("3.7 FWVGA slider")) { //$NON-NLS-1$ + // Fix metadata: this one entry doesn't have "in" like the rest of them + name = "3.7in FWVGA slider"; //$NON-NLS-1$ + } + + Matcher matcher = PATTERN.matcher(name); + if (matcher.matches()) { + String size = matcher.group(1); + String n = matcher.group(2); + int dot = size.indexOf('.'); + if (dot == -1) { + size = size + ".0"; + dot = size.length() - 2; + } + if (leadZeroes && dot < 3) { + // Pad to have at least 3 digits before the dot, for sorting purposes. + // We can revisit this once we get devices that are more than 999 inches wide. + size = "000".substring(dot) + size; + } + name = size + "\" " + n; + } + + return name; + } + + /** + * Returns the currently selected cell info in the table or null + */ + private CellInfo getTableSelection() { + if (mTable.isDisposed()) { + return null; + } + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (CellInfo) mTable.getItem(selIndex).getData(); + } + + return null; + } + + private void updateButtonStates() { + CellInfo ci = getTableSelection(); + + mNewButton.setEnabled(true); + mEditButton.setEnabled(ci != null); + mEditButton.setText((ci != null && !ci.mIsUser) ? "Clone..." : "Edit..."); + mDeleteButton.setEnabled(ci != null && ci.mIsUser); + mNewAvdButton.setEnabled(ci != null); + mRefreshButton.setEnabled(true); + } + + private void onNewDevice() { + DeviceCreationDialog dlg = new DeviceCreationDialog( + getShell(), + mDeviceManager, + mSdkContext.getSdkHelper().getImageFactory(), + null /*device*/); + if (dlg.open() == Window.OK) { + onRefresh(); + + // Select the new device, if any. + selectCellByDevice(dlg.getCreatedDevice()); + updateButtonStates(); + } + } + + private void onEditDevice() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null) { + return; + } + + DeviceCreationDialog dlg = new DeviceCreationDialog( + getShell(), + mDeviceManager, + mSdkContext.getSdkHelper().getImageFactory(), + ci.mDevice); + if (dlg.open() == Window.OK) { + onRefresh(); + + // Select the new device, if any. + selectCellByDevice(dlg.getCreatedDevice()); + updateButtonStates(); + } + } + + private void onDeleteDevice() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null || !ci.mIsUser) { + return; + } + + final String name = getPrettyName(ci.mDevice, false /*leadZeroes*/); + final AtomicBoolean result = new AtomicBoolean(false); + getDisplay().syncExec(new Runnable() { + @Override + public void run() { + Shell shell = getDisplay().getActiveShell(); + boolean ok = MessageDialog.openQuestion(shell, + "Delete Device Definition", + String.format( + "Please confirm that you want to delete the device definition named '%s'. This operation cannot be reverted.", + name)); + result.set(ok); + } + }); + + if (result.get()) { + mDeviceManager.removeUserDevice(ci.mDevice); + mDeviceManager.saveUserDevices(); + onRefresh(); + } + } + + private void onCreateAvd() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null) { + return; + } + + final AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), + mSdkContext, + mSdkTargets, + null); + dlg.selectInitialDevice(ci.mDevice); + + if (dlg.open() == Window.OK) { + onRefresh(); + + if (mAvdCreatedListener != null) { + mAvdCreatedListener.onAvdCreated(dlg.getCreatedAvd()); + } + } + } + + private void onRefresh() { + if (mDisableRefresh || mTable.isDisposed()) { + return; + } + int selIndex = mTable.getSelectionIndex(); + CellInfo selected = getTableSelection(); + + fillTable(mTable); + + if (selected != null) { + if (selectCellByName(selected)) { + updateButtonStates(); + return; + } + } + // If not found by name, use the position if available. + if (selIndex >= 0 && selIndex < mTable.getItemCount()) { + mTable.select(selIndex); + } + } + + private boolean selectCellByName(CellInfo selected) { + if (mTable.isDisposed() || selected == null || selected.mDevice == null) { + return false; + } + String name = selected.mDevice.getDisplayName(); + for (int n = mTable.getItemCount() - 1; n >= 0; n--) { + TableItem item = mTable.getItem(n); + Object data = item.getData(); + if (data instanceof CellInfo) { + CellInfo ci = (CellInfo) data; + if (ci != null && ci.mDevice != null && name.equals(ci.mDevice.getDisplayName())) { + // Same cell object. Select it. + mTable.select(n); + return true; + } + } + } + return false; + } + + private boolean selectCellByDevice(Device selected) { + if (mTable.isDisposed() || selected == null) { + return false; + } + for (int n = mTable.getItemCount() - 1; n >= 0; n--) { + TableItem item = mTable.getItem(n); + Object data = item.getData(); + if (data instanceof CellInfo) { + CellInfo ci = (CellInfo) data; + if (ci != null && ci.mDevice == selected) { + // Same device object. Select it. + mTable.select(n); + return true; + } + } + } + return false; + } + + // ------- + + + // --- Implementation of ISdkChangeListener --- + + @Override + public void onSdkReload() { + onRefresh(); + } + + @Override + public void preInstallHook() { + // nothing to be done for now. + } + + @Override + public void postInstallHook() { + // nothing to be done for now. + } + + // --- Implementation of DevicesChangeListener + + @Override + public void onDevicesChanged() { + onRefresh(); + } + + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java new file mode 100644 index 00000000..2e79601c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java @@ -0,0 +1,379 @@ +/* + * 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.sdkuilib.internal.repository.ui; + +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.utils.ILogger; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + + +/** + * A floating log window that can be displayed or hidden by the main SDK Manager 2 window. + * It displays a log of the sdk manager operation (listing, install, delete) including + * any errors (e.g. network error or install/delete errors.) + *

+ * Since the SDK Manager will direct all log to this window, its purpose is to be + * opened by the main window at startup and left open all the time. When not needed + * the floating window is hidden but not closed. This way it can easily accumulate + * all the log. + */ +public class LogWindow implements ILogUiProvider { + + private Shell mParentShell; + private Shell mShell; + private Composite mRootComposite; + private StyledText mStyledText; + private Label mLogDescription; + private Button mCloseButton; + + private final ILogger mSecondaryLog; + private boolean mCloseRequested; + private boolean mInitPosition = true; + private String mLastLogMsg = null; + + private enum TextStyle { + DEFAULT, + TITLE, + ERROR + } + + /** + * Creates the floating window. Callers should use {@link #open()} later. + * + * @param parentShell Parent container + * @param secondaryLog An optional logger where messages will also be output. + */ + public LogWindow(Shell parentShell, ILogger secondaryLog) { + mParentShell = parentShell; + mSecondaryLog = secondaryLog; + } + + /** + * For testing only. See {@link #open()} and {@link #close()} for normal usage. + * @wbp.parser.entryPoint + */ + void openBlocking() { + open(); + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + close(); + } + + /** + * Opens the window. + * This call does not block and relies on the fact that the main window is + * already running an SWT event dispatch loop. + * Caller should use {@link #close()} later. + */ + public void open() { + createShell(); + createContents(); + mShell.open(); + mShell.layout(); + mShell.setVisible(false); + } + + /** + * Closes and destroys the window. + * This must be called just before quitting the app. + *

+ * To simply hide/show the window, use {@link #setVisible(boolean)} instead. + */ + public void close() { + if (mShell != null && !mShell.isDisposed()) { + mCloseRequested = true; + mShell.close(); + mShell = null; + } + } + + /** + * Determines whether the window is currently shown or not. + * + * @return True if the window is shown. + */ + public boolean isVisible() { + return mShell != null && !mShell.isDisposed() && mShell.isVisible(); + } + + /** + * Toggles the window visibility. + * + * @param visible True to make the window visible, false to hide it. + */ + public void setVisible(boolean visible) { + if (mShell != null && !mShell.isDisposed()) { + mShell.setVisible(visible); + if (visible && mInitPosition) { + mInitPosition = false; + positionWindow(); + } + } + } + + private void createShell() { + mShell = new Shell(mParentShell, SWT.SHELL_TRIM | SWT.TOOL); + mShell.setMinimumSize(new Point(600, 300)); + mShell.setSize(450, 300); + mShell.setText("Android SDK Manager Log"); + GridLayoutBuilder.create(mShell); + + mShell.addShellListener(new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + if (!mCloseRequested) { + e.doit = false; + setVisible(false); + } + } + }); + } + + /** + * Create contents of the dialog. + */ + private void createContents() { + mRootComposite = new Composite(mShell, SWT.NONE); + GridLayoutBuilder.create(mRootComposite).columns(2); + GridDataBuilder.create(mRootComposite).fill().grab(); + + mStyledText = new StyledText(mRootComposite, + SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + GridDataBuilder.create(mStyledText).hSpan(2).fill().grab(); + + mLogDescription = new Label(mRootComposite, SWT.NONE); + GridDataBuilder.create(mLogDescription).hFill().hGrab(); + + mCloseButton = new Button(mRootComposite, SWT.NONE); + mCloseButton.setText("Close"); + mCloseButton.setToolTipText("Closes the log window"); + mCloseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setVisible(false); //$hide$ + } + }); + } + + // --- Implementation of ILogUiProvider --- + + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + mLogDescription.setText(description); + + if (acceptLog(description, true /*isDescription*/)) { + appendLine(TextStyle.TITLE, description); + + if (mSecondaryLog != null) { + mSecondaryLog.info("%1$s", description); //$NON-NLS-1$ + } + } + } + }); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, log); + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ + } + } + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.ERROR, log); + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.error(null, "%1$s", log); //$NON-NLS-1$ + } + } + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, " " + log); //$NON-NLS-1$ + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ + } + } + } + + + // ---- + + + /** + * Centers the dialog in its parent shell. + */ + private void positionWindow() { + // Centers the dialog in its parent shell + Shell child = mShell; + if (child != null && mParentShell != null) { + // get the parent client area with a location relative to the display + Rectangle parentArea = mParentShell.getClientArea(); + Point parentLoc = mParentShell.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + Point childSize = child.getSize(); + int cw = Math.max(childSize.x, pw); + int ch = childSize.y; + + int x = 30 + px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < py) y = py; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + } + + private void appendLine(TextStyle style, String text) { + if (!text.endsWith("\n")) { //$NON-NLS-1$ + text += '\n'; + } + + int start = mStyledText.getCharCount(); + + if (style == TextStyle.DEFAULT) { + mStyledText.append(text); + + } else { + mStyledText.append(text); + + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = text.length(); + sr.fontStyle = SWT.BOLD; + if (style == TextStyle.ERROR) { + sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED); + } + sr.underline = false; + mStyledText.setStyleRange(sr); + } + + // Scroll caret if it was already at the end before we added new text. + // Ideally we would scroll if the scrollbar is at the bottom but we don't + // have direct access to the scrollbar without overriding the SWT impl. + if (mStyledText.getCaretOffset() >= start) { + mStyledText.setSelection(mStyledText.getCharCount()); + } + } + + + private void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(runnable); + } + } + + /** + * Filter messages displayed in the log:
+ * - Messages with a % are typical part of a progress update and shouldn't be in the log.
+ * - Messages that are the same as the same output message should be output a second time. + * + * @param msg The potential log line to print. + * @return True if the log line should be printed, false otherwise. + */ + private boolean acceptLog(String msg, boolean isDescription) { + if (msg == null) { + return false; + } + + msg = msg.trim(); + + // Descriptions also have the download progress status (0..100%) which we want to avoid + if (isDescription && msg.indexOf('%') != -1) { + return false; + } + + if (msg.equals(mLastLogMsg)) { + return false; + } + + mLastLogMsg = msg; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java new file mode 100644 index 00000000..b83719dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java @@ -0,0 +1,1216 @@ +/* + * 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.sdkuilib.internal.repository.ui; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTreeViewer; +import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.jface.window.ToolTip; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +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.Image; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import com.android.annotations.NonNull; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.PackageOperation; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager.RepoLoadedCallback; +import com.android.repository.api.Uninstaller; +import com.android.repository.impl.meta.Archive; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.installer.SdkInstallerUtil; +import com.android.sdklib.repository.meta.DetailsTypes.PlatformDetailsType; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.LoadPackagesRequest; +import com.android.sdkuilib.internal.repository.PackageInstallListener; +import com.android.sdkuilib.internal.repository.PackageManager; +import com.android.sdkuilib.internal.repository.content.CategoryKeyType; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.content.PackageContentProvider; +import com.android.sdkuilib.internal.repository.content.PackageFilter; +import com.android.sdkuilib.internal.repository.content.PackageInstaller; +import com.android.sdkuilib.internal.repository.content.PackageType; +import com.android.sdkuilib.internal.repository.content.PkgCategory; +import com.android.sdkuilib.internal.repository.content.PkgCellAgent; +import com.android.sdkuilib.internal.repository.content.PkgCellLabelProvider; +import com.android.sdkuilib.internal.repository.content.PkgItem; +import com.android.sdkuilib.internal.repository.content.PkgTreeColumnViewerLabelProvider; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.internal.widgets.PackageTypesSelector; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +/** + * Page that displays both locally installed packages as well as all known + * remote available packages. This gives an overview of what is installed + * vs what is available and allows the user to update or install packages. + */ +public final class PackagesPage extends Composite { + + enum MenuAction { + RELOAD (SWT.NONE, "Reload"), + //TOGGLE_SHOW_INSTALLED_PKG (SWT.CHECK, "Show Installed Packages"), + TOGGLE_SHOW_OBSOLETE_PKG (SWT.CHECK, "Show Obsolete Packages"), + TOGGLE_SHOW_NEW_PKG (SWT.CHECK, "Show New Packages"), + FILTER_PACKAGES (SWT.NONE, "Filter Packages"); + + private final int mMenuStyle; + private final String mMenuTitle; + + MenuAction(int menuStyle, String menuTitle) { + mMenuStyle = menuStyle; + mMenuTitle = menuTitle; + } + + public int getMenuStyle() { + return mMenuStyle; + } + + public String getMenuTitle() { + return mMenuTitle; + } + }; + + // Column ids + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; + + private final Map mMenuActions = new HashMap(); + + private final SdkContext mSdkContext; + //private final SdkInvocationContext mContext; + private final PackageAnalyser mPackageAnalyser; + + private boolean mDisplayArchives = false; + private boolean mOperationPending; + private ProgressRunner mProgressRunner; + private Composite mGroupPackages; + private Text mTextSdkOsPath; + private Button mCheckFilterObsolete; + //private Button mCheckFilterInstalled; + private Button mCheckFilterNew; + private Button mCheckAll; + private Composite mGroupOptions; + private Composite mGroupSdk; + private Button mButtonDelete; + private Button mButtonInstall; + private Button mButtonPkgTypes; + private Button mButtonCancel; + private Font mTreeFontItalic; + private TreeColumn mTreeColumnName; + private CheckboxTreeViewer mTreeViewer; + private ILogUiProvider mSdkProgressControl; + private ITaskFactory mTaskFactory; + private PackageFilter mPackageFilter; + private SdkProgressFactory factory; + + public PackagesPage( + Composite parent, + int swtStyle, + SdkContext sdkContext, + SdkInvocationContext context, + Set packageTypeSet) + { + super(parent, swtStyle); + mSdkContext = sdkContext; + mPackageFilter = new PackageFilter(packageTypeSet); + mPackageAnalyser = new PackageAnalyser(sdkContext); + //mContext = context; + createContents(this); + postCreate(); + } + + public void onReady(SdkProgressFactory factory) { + this.factory = factory; + mProgressRunner = factory; + mSdkProgressControl = factory.getProgressControl(); + mTaskFactory = factory; + startLoadPackages(); + } + + public void onSdkReload() { + startLoadPackages(); + } + + private void createContents(Composite parent) + { + Color foreColor = parent.getForeground(); + Color backColor = parent.getBackground(); + Display display = parent.getDisplay(); + Color hiForeColor = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND); + Color hiBackColor = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND); + GridLayoutBuilder.create(parent).noMargins().columns(2); + + mGroupSdk = new Composite(parent, SWT.NONE); + GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2); + GridLayoutBuilder.create(mGroupSdk).columns(2); + + Label label1 = new Label(mGroupSdk, SWT.NONE); + label1.setText("SDK Path:"); + + mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE); + GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab(); + mTextSdkOsPath.setEnabled(false); + + Group groupPackages = new Group(parent, SWT.SHADOW_NONE); + mGroupPackages = groupPackages; + GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2); + groupPackages.setText("Packages"); + GridLayoutBuilder.create(groupPackages).columns(2); + + mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER); + mTreeViewer.addFilter(new ViewerFilter() + { + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + return filterViewerItem(element); + } + }); + + mTreeViewer.addCheckStateListener(new ICheckStateListener() + { + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + onTreeCheckStateChanged(event); //$hide$ + } + }); + + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() + { + @Override + public void doubleClick(DoubleClickEvent event) { + onTreeDoubleClick(event); //$hide$ + } + }); + + Tree tree = mTreeViewer.getTree(); + tree.setLinesVisible(true); + tree.setHeaderVisible(true); + GridDataBuilder.create(tree).hSpan(2).fill().grab(); + + // column name icon is set when loading depending on the current filter type + // (e.g. API level or source) + TreeViewerColumn columnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); + mTreeColumnName = columnName.getColumn(); + mTreeColumnName.setText("Name"); + mTreeColumnName.setWidth(400); + + TreeViewerColumn columnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn2 = columnApi.getColumn(); + treeColumn2.setText("API"); + treeColumn2.setAlignment(SWT.LEFT); + treeColumn2.setWidth(35); + + TreeViewerColumn columnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn3 = columnRevision.getColumn(); + treeColumn3.setText("Revision"); + treeColumn3.setToolTipText("Revision currently installed"); + treeColumn3.setAlignment(SWT.LEFT); + treeColumn3.setWidth(80); + + + TreeViewerColumn columnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn4 = columnStatus.getColumn(); + treeColumn4.setText("Status"); + treeColumn4.setAlignment(SWT.LEAD); + treeColumn4.setWidth(205); + + mGroupOptions = new Group(groupPackages, SWT.SHADOW_OUT); + GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); + GridLayoutBuilder.create(mGroupOptions).columns(6).noMargins(); + + // Options line 1, 6 columns + + Label label3 = new Label(mGroupOptions, SWT.NONE); + label3.setText("Show:"); + GridDataBuilder.create(label3).vSpan(2).vTop(); + mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK); + GridDataBuilder.create(mCheckFilterNew).vTop(); + mCheckFilterNew.setText("New"); + mCheckFilterNew.setToolTipText("Show latest available new packages"); + mCheckFilterNew.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent event) { + // Only enable check all if "New" is checked + mCheckAll.setEnabled(((Button)event.getSource()).getSelection()); + refreshViewerInput(); + } + }); + mCheckFilterNew.setSelection(true); + mCheckAll = new Button(mGroupOptions, SWT.CHECK); + GridDataBuilder.create(mCheckAll).vSpan(2).vTop(); + mCheckAll.setText("All"); + mCheckAll.setToolTipText("Show all available new packages"); + mCheckAll.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + refreshViewerInput(); + } + }); +/* + mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK); + mCheckFilterInstalled.setToolTipText("Show Installed"); + mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + refreshViewerInput(); + } + }); + mCheckFilterInstalled.setSelection(true); + mCheckFilterInstalled.setText("Installed"); +*/ + //new Label(mGroupOptions, SWT.NONE); + + Label label4 = new Label(mGroupOptions, SWT.NONE); + label4.setText("Select:"); + GridDataBuilder.create(label4).vSpan(2).vTop(); + Link linkSelectUpdates = new Link(mGroupOptions, SWT.NONE); + linkSelectUpdates.setText("Select Updates"); + linkSelectUpdates.setToolTipText("Selects all items that are updates."); + //GridDataBuilder.create(linkSelectUpdates).hFill(); + linkSelectUpdates.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + onSelectPackages(true, false); // selectTop + } + }); + + // placeholder between "select all" and "install" + //Label placeholder = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder).hFill().hGrab(); + + mButtonInstall = new Button(mGroupOptions, SWT.NONE); + mButtonInstall.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() + mButtonInstall.setToolTipText("Install one or more packages"); + GridDataBuilder.create(mButtonInstall).vCenter().wHint(150).hFill().hGrab().hRight(); + mButtonInstall.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + onButtonInstall(); //$hide$ + } + }); + + // Options line 2, 6 columns + + //Label placeholder2 = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder2).hFill().hGrab(); + + mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK); + mCheckFilterObsolete.setText("Obsolete"); + mCheckFilterObsolete.setToolTipText("Also show obsolete packages"); + mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent event) { + if (((Button)event.getSource()).getSelection()) { + mCheckFilterObsolete.setForeground(hiForeColor); + mCheckFilterObsolete.setBackground(hiBackColor); + } else { + mCheckFilterObsolete.setForeground(foreColor); + mCheckFilterObsolete.setBackground(backColor); + } + refreshViewerInput(); + } + }); + mCheckFilterObsolete.setSelection(false); + + // placeholder before "deselect" + //new Label(mGroupOptions, SWT.NONE); + //new Label(mGroupOptions, SWT.NONE); + + Link linkDeselect = new Link(mGroupOptions, SWT.NONE); + linkDeselect.setText("Deselect All"); + linkDeselect.setToolTipText("Deselects all the currently selected items"); + //GridDataBuilder.create(linkDeselect).hFill(); + linkDeselect.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + onDeselectAll(); + } + }); + + // placeholder between "deselect" and "delete" + //placeholder = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder).hFill().hGrab(); + + mButtonDelete = new Button(mGroupOptions, SWT.NONE); + mButtonDelete.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() + mButtonDelete.setToolTipText("Delete one ore more installed packages"); + GridDataBuilder.create(mButtonDelete).vCenter().wHint(150).hFill().hGrab().hRight(); + mButtonDelete.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + onButtonDelete(); //$hide$ + } + }); + mGroupOptions.pack(); + Group controls = new Group(groupPackages, SWT.NONE); + GridDataBuilder.create(controls).vCenter(); + GridLayoutBuilder.create(controls); + mButtonPkgTypes = new Button(controls, SWT.NONE); + mButtonPkgTypes.setText("Package Types..."); + GridDataBuilder.create(mButtonPkgTypes).wHint(100); + mButtonPkgTypes.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + selectPackages(); + } + }); + mButtonCancel = new Button(controls, SWT.NONE); + mButtonCancel.setText("Cancel"); + GridDataBuilder.create(mButtonCancel).wHint(100); + mButtonCancel.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + mSdkContext.getProgressIndicator().cancel(); //$hide$ + getShell().close(); + } + }); + controls.pack(); + FontData fontData = tree.getFont().getFontData()[0]; + fontData.setStyle(SWT.ITALIC); + mTreeFontItalic = new Font(tree.getDisplay(), fontData); + tree.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mTreeFontItalic.dispose(); + mTreeFontItalic = null; + } + }); + mTreeViewer.setContentProvider(new PackageContentProvider(mTreeViewer, mPackageFilter)); + PkgCellAgent pkgCellAgent = new PkgCellAgent(mSdkContext, mPackageAnalyser, mTreeFontItalic); + columnApi.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.API))); + columnName.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.NAME))); + columnStatus.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.STATUS))); + columnRevision.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.REVISION))); + } + + protected void selectPackages() { + PackageTypesSelector pkgTypesSelector = new PackageTypesSelector(getShell(), mPackageFilter.getPackageTypes()); + if (pkgTypesSelector.open()) { + mPackageFilter.setPackageTypes(pkgTypesSelector.getPackageTypeSet()); + refreshViewerInput(); + } + } + + private Image getImage(String filename) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + return imgFactory.getImageByName(filename); + } + return null; + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + + // --- menu interactions --- + + protected void registerMenuAction(final MenuAction action, MenuItem item) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Button button = null; + + switch (action) { + case RELOAD: + startLoadPackages(); + break; +// case TOGGLE_SHOW_INSTALLED_PKG: +// button = mCheckFilterInstalled; +// break; + case TOGGLE_SHOW_OBSOLETE_PKG: + button = mCheckFilterObsolete; + break; + case TOGGLE_SHOW_NEW_PKG: + button = mCheckFilterNew; + break; + case FILTER_PACKAGES: + selectPackages(); + break; + } + + if (button != null && !button.isDisposed()) { + // Toggle this button (radio or checkbox) + + boolean value = button.getSelection(); + + // SWT doesn't automatically switch radio buttons when using the + // Widget#setSelection method, so we'll do it here manually. + if (!value && (button.getStyle() & SWT.RADIO) != 0) { + // we'll be selecting this radio button, so deselect all ther other ones + // in the parent group. + for (Control child : button.getParent().getChildren()) { + if (child instanceof Button && + child != button && + (child.getStyle() & SWT.RADIO) != 0) { + ((Button) child).setSelection(value); + } + } + } + + button.setSelection(!value); + + // SWT doesn't actually invoke the listeners when using Widget#setSelection + // so let's run the actual action. + button.notifyListeners(SWT.Selection, new Event()); + } + + updateMenuCheckmarks(); + } + }); + + mMenuActions.put(action, item); + } + + // --- internal methods --- + + private void updateMenuCheckmarks() { + for (Entry entry : mMenuActions.entrySet()) { + MenuAction action = entry.getKey(); + MenuItem item = entry.getValue(); + + if (action.getMenuStyle() == SWT.NONE) { + continue; + } + + boolean value = false; + Button button = null; + + switch (action) { + //case TOGGLE_SHOW_INSTALLED_PKG: + // button = mCheckFilterInstalled; + // break; + case TOGGLE_SHOW_OBSOLETE_PKG: + button = mCheckFilterObsolete; + break; + case TOGGLE_SHOW_NEW_PKG: + button = mCheckFilterNew; + break; + case RELOAD: + case FILTER_PACKAGES: + // No checkmark to update + break; + } + + if (button != null && !button.isDisposed()) { + value = button.getSelection(); + } + + if (!item.isDisposed()) { + item.setSelection(value); + } + } + } + + private boolean postCreate() { + File sdkLocation = mSdkContext.getLocation(); + // Only show SDK location if is valid + if (sdkLocation.exists() && sdkLocation.isDirectory()) + mTextSdkOsPath.setText(sdkLocation.toString()); + + ((PackageContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( + mDisplayArchives); + + ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); + return true; + + } + + private void startLoadPackages() { + + if (mTreeColumnName.isDisposed()) { + // If the UI got disposed, don't try to load anything since we won't be + // able to display it anyway. + return; + } + // Packages will be loaded when onReady() is called + if (mProgressRunner == null) + return; + mTreeColumnName.setImage(getImage(PackagesPageIcons.ICON_SORT_BY_API)); + + PackageManager packageManager = mSdkContext.getPackageManager(); + LoadPackagesRequest loadPackagesRequest = new LoadPackagesRequest(mProgressRunner); + RepoLoadedCallback onSuccess = new RepoLoadedCallback(){ + @Override + public void doRun(RepositoryPackages packages) { + if (!(mGroupPackages == null || mGroupPackages.isDisposed())) + { + packageManager.setPackages(packages); + mPackageAnalyser.loadPackages(); + Collection localPackages = packageManager.getRepositoryPackages().getLocalPackagesForPrefix(PackageType.platforms.toString()); + boolean hasPlatform = false; + if (!localPackages.isEmpty()) { + Iterator iterator = localPackages.iterator(); + while (iterator.hasNext()) + if (iterator.next().getTypeDetails() instanceof PlatformDetailsType) { + hasPlatform = true; + break; + } + } + if (!hasPlatform) { + mSdkProgressControl.setDescription("No Android Platform is installed. Please select one and then click on \"Install\" button."); + Set packageTypeSet = new TreeSet<>(); + packageTypeSet.addAll(mPackageFilter.getPackageTypes()); + packageTypeSet.add(PackageType.platforms); + mPackageFilter.setPackageTypes(packageTypeSet); + + } else { + mSdkProgressControl.setDescription("Done loading packages."); + } + mGroupPackages.getDisplay().syncExec(new Runnable(){ + @Override + public void run() { + // automatically select all new and update packages. + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null || checked.length == 0) + onSelectPackages( + true, //selectUpdates, + true); //selectTop + refreshViewerInput(); + }}); + } + }}; + Runnable onError = new Runnable(){ + @Override + public void run() { + mSdkProgressControl.setDescription("Package operation did not complete due to error or cancellation"); + }}; + //loadPackagesRequest.setOnLocalComplete(Collections.singletonList(onLocalComplete)); + loadPackagesRequest.setOnSuccess(Collections.singletonList(onSuccess)); + loadPackagesRequest.setOnError(Collections.singletonList(onError)); + packageManager.requestRepositoryPackages(loadPackagesRequest); + } + + private void refreshViewerInput() { + if (!mGroupPackages.isDisposed()) { + try { + setViewerInput(); + } catch (Exception ignore) {} + + // set the initial expanded state + expandInitial(mTreeViewer.getInput()); + + updateButtonsState(); + updateMenuCheckmarks(); + } + } + + /** + * Invoked from {@link #refreshViewerInput()} to actually either set the + * input of the tree viewer or refresh it if it's the same input + * object. + */ + private void setViewerInput() { + List> cats = mPackageAnalyser.getApiCategories(); + if ((mTreeViewer.getInput() != cats) || mPackageFilter.isFilterOn()) { + // set initial input + if (mPackageFilter.isFilterOn()) + mTreeViewer.setInput(mPackageFilter.getFilteredApiCategories(cats)); + else + mTreeViewer.setInput(cats); + } else { + // refresh existing, which preserves the expanded state, the selection + // and the checked state. + mTreeViewer.refresh(); + } + } + + /** + * Decide whether to keep an item in the current tree based on user-chosen filter options. + */ + private boolean filterViewerItem(Object treeElement) { + boolean selectNew = mCheckFilterNew.getSelection(); + if (treeElement instanceof PkgCategory) { + PkgCategory cat = (PkgCategory) treeElement; + cat.setSelectAllPackages(selectNew && mCheckAll.getSelection()); + if (!cat.getItems().isEmpty()) { + // A category is hidden if all of its content is hidden. + // However empty categories are always visible. + for (PkgItem item : cat.getItems()) { + if (filterViewerItem(item)) { + // We found at least one element that is visible. + return true; + } + } + return false; + } + } + + if (treeElement instanceof PkgItem) { + PkgItem item = (PkgItem) treeElement; + + if (!mCheckFilterObsolete.getSelection()) { + if (item.isObsolete()) { + return false; + } + } +/* + if (!mCheckFilterInstalled.getSelection()) { + if (item.getState() == PkgState.INSTALLED) { + return false; + } + } +*/ + if (!selectNew) { + if (item.getState() == PkgState.NEW ) { //|| item.hasUpdatePkg() + return false; + } + } + } + + return true; + } + + /** + * Performs the initial expansion of the tree. This expands categories that contain + * at least one installed item and collapses the ones with nothing installed. + * + * TODO: change this to only change the expanded state on categories that have not + * been touched by the user yet. Once we do that, call this every time a new source + * is added or the list is reloaded. + */ + private void expandInitial(Object elem) { + if (elem == null) { + return; + } + if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { + + boolean enablePreviews = + mSdkContext.getSettings().getEnablePreviews(); + + mTreeViewer.setExpandedState(elem, true); + nextCategory: for (Object pkg : + ((ITreeContentProvider) mTreeViewer.getContentProvider()). + getChildren(elem)) { + if (pkg instanceof PkgCategory) { + PkgCategory cat = (PkgCategory) pkg; + // Always expand the Tools category (and the preview one, if enabled) + if ((cat.getKeyType() == CategoryKeyType.TOOLS) || + (enablePreviews && + (cat.getKeyType() == CategoryKeyType.TOOLS_PREVIEW))) { + expandInitial(pkg); + continue nextCategory; + } + for (PkgItem item : cat.getItems()) { + if (item.getState() == PkgState.INSTALLED) { + expandInitial(pkg); + continue nextCategory; + } + } + } + } + } + } + + /** + * Handle checking and unchecking of the tree items. + * + * When unchecking, all sub-tree items checkboxes are cleared too. + * When checking a source, all of its packages are checked too. + * When checking a package, only its compatible archives are checked. + */ + private void onTreeCheckStateChanged(CheckStateChangedEvent event) { + boolean checked = event.getChecked(); + Object elem = event.getElement(); + + assert event.getSource() == mTreeViewer; + + // When selecting, we want to only select compatible archives and expand the super nodes. + checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/); + updateButtonsState(); + } + + private void onTreeDoubleClick(DoubleClickEvent event) { + assert event.getSource() == mTreeViewer; + ISelection sel = event.getSelection(); + if (sel.isEmpty() || !(sel instanceof ITreeSelection)) { + return; + } + ITreeSelection tsel = (ITreeSelection) sel; + Object elem = tsel.getFirstElement(); + if (elem == null) { + return; + } + + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); + Object[] children = provider.getElements(elem); + if (children == null) { + return; + } + + if (children.length > 0) { + // If the element has children, expand/collapse it. + if (mTreeViewer.getExpandedState(elem)) { + mTreeViewer.collapseToLevel(elem, 1); + } else { + mTreeViewer.expandToLevel(elem, 1); + } + } else { + // If the element is a terminal one, select/deselect it. + checkAndExpandItem( + elem, + !mTreeViewer.getChecked(elem), + false /*fixChildren*/, + true /*fixParent*/); + updateButtonsState(); + } + } + + private void checkAndExpandItem( + Object elem, + boolean checked, + boolean fixChildren, + boolean fixParent) { + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); + + // fix the item itself + if (checked != mTreeViewer.getChecked(elem)) { + mTreeViewer.setChecked(elem, checked); + } + if (elem instanceof PkgItem) { + // update the PkgItem to reflect the selection + ((PkgItem) elem).setChecked(checked); + } + + if (!checked) { + if (fixChildren) { + // when de-selecting, we deselect all children too + mTreeViewer.setSubtreeChecked(elem, checked); + for (Object child : provider.getChildren(elem)) { + checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/); + } + } + + // fix the parent when deselecting + if (fixParent) { + Object parent = provider.getParent(elem); + if (parent != null && mTreeViewer.getChecked(parent)) { + mTreeViewer.setChecked(parent, false); + } + } + return; + } + + // When selecting, we also select sub-items (for a category) + if (fixChildren) { + if (elem instanceof PkgCategory || elem instanceof PkgItem) { + Object[] children = provider.getChildren(elem); + for (Object child : children) { + checkAndExpandItem(child, true, fixChildren, false/*fixParent*/); + } + // only fix the parent once the last sub-item is set + if (elem instanceof PkgCategory) { + if (children.length > 0) { + checkAndExpandItem( + children[0], true, false/*fixChildren*/, true/*fixParent*/); + } else { + mTreeViewer.setChecked(elem, false); + } + } + } else if (elem instanceof Package) { + // in details mode, we auto-select compatible packages + selectCompatibleArchives(elem, provider); + } + } + + if (fixParent && checked && elem instanceof PkgItem) { + Object parent = provider.getParent(elem); + if (!mTreeViewer.getChecked(parent)) { + Object[] children = provider.getChildren(parent); + boolean allChecked = children.length > 0; + for (Object e : children) { + if (!mTreeViewer.getChecked(e)) { + allChecked = false; + break; + } + } + if (allChecked) { + mTreeViewer.setChecked(parent, true); + } + } + } + } + + private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) { + for (Object archive : provider.getChildren(pkg)) { + if (archive instanceof Archive) { + mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible()); + } + } + } + + /** + * Mark packages as checked according to selection criteria. + */ + private void onSelectPackages(boolean selectUpdates, boolean selectTop) { + // This will update the tree's "selected" state and then invoke syncViewerSelection() + // which will in turn update tree. + mPackageAnalyser.checkNewUpdateItems( + selectUpdates, + selectTop); + mTreeViewer.setInput(mPackageAnalyser.getApiCategories()); + syncViewerSelection(); + } + + /** + * Deselect all checked PkgItems. + */ + private void onDeselectAll() { + mPackageAnalyser.uncheckAllItems(); + syncViewerSelection(); + } + + /** + * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state. + */ + private void syncViewerSelection() { + ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); + + Object input = mTreeViewer.getInput(); + if (input != null) { + for (Object cat : provider.getElements(input)) { + Object[] children = provider.getElements(cat); + boolean allChecked = children.length > 0; + for (Object child : children) { + if (child instanceof PkgItem) { + PkgItem item = (PkgItem) child; + boolean checked = item.isChecked(); + allChecked &= checked; + + if (checked != mTreeViewer.getChecked(item)) { + if (checked) { + if (!mTreeViewer.getExpandedState(cat)) { + mTreeViewer.setExpandedState(cat, true); + } + } + checkAndExpandItem( + item, + checked, + true/*fixChildren*/, + false/*fixParent*/); + } + } + } + + if (allChecked != mTreeViewer.getChecked(cat)) { + mTreeViewer.setChecked(cat, allChecked); + } + } + } + + updateButtonsState(); + } + + /** + * Indicate an install/delete operation is pending. + * This disables the install/delete buttons. + * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block. + */ + private void beginOperationPending() { + mOperationPending = true; + updateButtonsState(); + } + + private void endOperationPending() { + mOperationPending = false; + updateButtonsState(); + } + + /** + * Updates the Install and Delete Package buttons. + */ + private void updateButtonsState() { + if (!mButtonInstall.isDisposed()) { + int numPackages = getPackagesForInstall(null /*archives*/); + + mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending); + mButtonInstall.setText( + numPackages == 0 ? "Install packages..." : // disabled button case + numPackages == 1 ? "Install 1 package..." : + String.format("Install %d packages...", numPackages)); + } + + if (!mButtonDelete.isDisposed()) { + // We can only delete local archives + int numPackages = getPackagesToDelete(null /*outMsg*/, null /*outArchives*/); + + mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending); + mButtonDelete.setText( + numPackages == 0 ? "Delete packages..." : // disabled button case + numPackages == 1 ? "Delete 1 package..." : + String.format("Delete %d packages...", numPackages)); + } + } + + /** + * Called when the Install Package button is selected. + * Collects the packages to be installed and shows the installation window. + */ + private void onButtonInstall() { + beginOperationPending(); + List requiredPackages = new ArrayList<>(); + getPackagesForInstall(requiredPackages); + PackageInstaller packageInstaller = new PackageInstaller(requiredPackages, factory); + packageInstaller.installPackages(getShell(), mSdkContext, new PackageInstallListener(){ + + @Override + public void onPackagesInstalled(int count) { + Display.getDefault().syncExec(new Runnable(){ + + @Override + public void run() { + endOperationPending(); + // The local package list has changed, make sure to refresh it + startLoadPackages(); + mButtonCancel.setText("OK"); + }}); + }}); + } + + /** + * Selects the packages that can be installed. + * This can be used with a null {@code outPackageItems} just to count the number of + * installable packages. + * + * @param outPackageItems A package item list to return remote packages. + * This can be null. + * @return The number of archives that can be installed. + */ + private int getPackagesForInstall(List outPackageItems) { + if (mTreeViewer == null || + mTreeViewer.getTree() == null || + mTreeViewer.getTree().isDisposed()) { + return 0; + } + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null) { + return 0; + } + int count = 0; + for (Object c : checked) { + if (c instanceof PkgItem) { + PkgItem packageItem = (PkgItem)c; + RemotePackage remotePackage = null; + if (packageItem.hasUpdatePkg()) { + remotePackage = packageItem.getUpdatePkg().getRemote(); + } else if (packageItem.getState() == PkgState.NEW) { + remotePackage = (RemotePackage) packageItem.getMainPackage(); + } + if (remotePackage != null) { + count++; + if (outPackageItems != null) { + outPackageItems.add(packageItem); + } + } + } + } + return count; + } + + /** + * Called when the Delete Package button is selected. + * Collects the packages to be deleted, prompt the user for confirmation + * and actually performs the deletion. + */ + private void onButtonDelete() { + final String title = "Delete SDK Package"; + StringBuilder msg = new StringBuilder("Are you sure you want to delete:"); + + // A list of package items to delete + final ArrayList outPackageItems = new ArrayList(); + + getPackagesToDelete(msg, outPackageItems); + + if (!outPackageItems.isEmpty()) { + msg.append("\n").append("This cannot be undone."); //$NON-NLS-1$ + if (MessageDialog.openQuestion(getShell(), title, msg.toString())) { + beginOperationPending(); + Runnable onCompletion = new Runnable(){ + + @Override + public void run() { + endOperationPending(); + + // The local package list has changed, make sure to refresh it + startLoadPackages(); + mButtonCancel.setText("OK"); + }}; + mTaskFactory.start("Delete Package", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + monitor.setProgressMax(outPackageItems.size() + 1); + for (PkgItem packageItem : outPackageItems) { + LocalPackage localPackage = (LocalPackage)packageItem.getMainPackage(); + monitor.setDescription("Deleting '%1$s' (%2$s)", + localPackage.getDisplayName(), + localPackage.getPath()); + + // Delete the actual package + Uninstaller uninstaller = SdkInstallerUtil.findBestInstallerFactory(localPackage, mSdkContext.getHandler()) + .createUninstaller(localPackage, mSdkContext.getRepoManager(), mSdkContext.getFileOp()); + if (applyPackageOperation(uninstaller)) { + packageItem.markDeleted(); + } else { + // there was an error, abort. + monitor.error(null, "Uninstall of package failed due to an error"); + monitor.setProgressMax(0); + break; + } + monitor.incProgress(1); + if (monitor.isCancelRequested()) { + break; + } + } + + monitor.incProgress(1); + monitor.setDescription("Done"); + mPackageAnalyser.removeDeletedNodes(); + } + }, onCompletion); + } + } + } + + + private boolean applyPackageOperation( + @NonNull PackageOperation operation) { + ProgressIndicator progressIndicator = mSdkContext.getProgressIndicator(); + return operation.prepare(progressIndicator) && operation.complete(progressIndicator); + } + + /** + * Selects the archives that can be deleted and collect their names. + * This can be used with a null {@code outArchives} and a null {@code outMsg} + * just to count the number of archives to be deleted. + * + * @param outMsg A StringBuilder where the names of the packages to be deleted is + * accumulated. This is used to confirm deletion with the user. + * @param outPackageItems A package item list to return local packages + * This can be null. + * @return The number of packages that can be deleted. + */ + private int getPackagesToDelete(StringBuilder outMsg, List outPackageItems) { + if (mTreeViewer == null || + mTreeViewer.getTree() == null || + mTreeViewer.getTree().isDisposed()) { + return 0; + } + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null) { + // This should not happen since the button should be disabled + return 0; + } + + int count = 0; + for (Object c : checked) { + if (c instanceof PkgItem) { + PkgItem packageItem = (PkgItem) c; + PkgState state = packageItem.getState(); + if (state == PkgState.INSTALLED) { + LocalPackage localPackage = (LocalPackage)packageItem.getMainPackage(); + count++; + if (outMsg != null) { + File dir = new File(localPackage.getPath()); + if (dir.isDirectory()) { + outMsg.append("\n - ") //$NON-NLS-1$ + .append(localPackage.getDisplayName()); + } + } + if (outPackageItems != null) { + outPackageItems.add(packageItem); + } + } + } + } + return count; + } + + // --- End of hiding from SWT Designer --- + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java new file mode 100644 index 00000000..608bded6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java @@ -0,0 +1,32 @@ +/* + * 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.sdkuilib.internal.repository.ui; + + +/** + * Icons used by {@link PackagesPage}. + */ +public class PackagesPageIcons { + + public static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ + public static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ + public static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java new file mode 100644 index 00000000..02195af9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.repository.ui; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ProgressBar; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressIndicatorAdapter; +import com.android.repository.api.ProgressRunner; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.internal.tasks.ProgressTask; +import com.android.sdkuilib.internal.tasks.ProgressView; +import com.android.sdkuilib.internal.tasks.SdkProgressIndicator; +import com.android.utils.ILogger; + +/** + * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog + * for each new task. + */ +public class SdkProgressFactory extends ProgressIndicatorAdapter implements ITaskFactory, ILogger, ProgressRunner { + + public interface ISdkLogWindow + { + void log(String log); + void logVerbose(String log); + void logError(String log); + void setDescription(String description); + void show(); + } + + private final ProgressView progressView; + private final ISdkLogWindow logWindow; + private int referenceCount; + + /** + * Creates a new {@link ProgressView} object, a simple "holder" for the various + * widgets used to display and update a progress + status bar. This object is + * provided to the factory. + * + * @param statusText The label to display titles of status updates (e.g. task titles and + * calls to {@link #setDescription(String)}.) Must not be null. + * @param progressBar The progress bar to update during a task. Must not be null. + * @param stopButton The stop button. It will be disabled when there's no task that can + * be interrupted. A selection listener will be attached to it. Optional. Can be null. + * @param logWindow Log adapter which can be requested to become visible when errors are logged + */ + public SdkProgressFactory( + Label statusText, + ProgressBar progressBar, + Control stopButton, + ISdkLogWindow logWindow) { + this.logWindow = logWindow; + this.progressView = new ProgressView(statusText, progressBar, stopButton, getLogUiProvider(logWindow)); + } + + public ILogUiProvider getProgressControl() + { + return progressView; + } + + /** + * Starts a new task with a new {@link ITaskMonitor}. + * The task will execute asynchronously in a job. + * @param title The title of the task, displayed in the monitor if any. + * @param task The task to run. + * @param onTerminateTask Callback when task done + */ + @Override + public void start(String title, ITask task, Runnable onTerminateTask) { + progressView.startAsyncTask(title, task, onTerminateTask); + } + + // Returns object which delegates all logging to the logWindow window + // and filters errors to make sure the window is visible when + // an error is logged. + private ILogUiProvider getLogUiProvider(ISdkLogWindow logWindow) + { + return new ILogUiProvider() { + @Override + public void setDescription(String description) { + logWindow.setDescription(description); + } + + @Override + public void log(String log) { + logWindow.log(log); + } + + @Override + public void logVerbose(String log) { + logWindow.logVerbose(log); + } + + @Override + public void logError(String log) { + logWindow.logError(log); + logWindow.show(); + }}; + } + + // --- ILogger interface ---- // + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + StringWriter builder = new StringWriter(); + if (errorFormat != null) + builder.append(String.format("Error: " + errorFormat, arg)); + + if (throwable != null) { + if (errorFormat == null) + builder.append("Error: ").append(throwable.getMessage()); + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + logWindow.logError(builder.toString()); + logWindow.show(); + } + + + @Override + public void info(String errorFormat, Object... arg) { + logWindow.log(String.format(errorFormat, arg)); + } + + + @Override + public void verbose(String errorFormat, Object... arg) { + logWindow.logVerbose(String.format(errorFormat, arg)); + } + + + @Override + public void warning(String errorFormat, Object... arg) { + // TODO - Add warning level + logWindow.log(String.format(errorFormat, arg)); + } + + // --- Logger component of IProgressIndicator interface ---- // + /** + * Logs a warning. + */ + @Override + public void logWarning(@NonNull String s) { + logWindow.log(s); + } + + /** + * Logs a warning, including a stacktrace. + */ + @Override + public void logWarning(@NonNull String s, @Nullable Throwable throwable) { + StringWriter builder = new StringWriter(); + builder.append(s); + if (throwable != null) { + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + logWindow.log(builder.toString()); + } + + /** + * Logs an error. + */ + @Override + public void logError(@NonNull String s) { + error(null, s); + } + + /** + * Logs an error, including a stacktrace. + */ + @Override + public void logError(@NonNull String s, @Nullable Throwable throwable) { + error(throwable, s); + } + + /** + * Logs an info message. + */ + @Override + public void logInfo(@NonNull String s) { + logWindow.log(s); + } + + @Override + public void logVerbose(@NonNull String s) { + logWindow.logVerbose(s); + } + + + @Override + public void runAsyncWithProgress(final ProgressRunnable progressRunnable) { + String title = this.getClass().getSimpleName() + referenceCount++; + ProgressRunnable monitor = new ProgressRunnable(){ + + @Override + public void run(ProgressIndicator indicator, ProgressRunner runner) { + try + { + progressRunnable.run(indicator, runner); + } + finally + { + progressView.endTask(); + } + }}; + progressView.startAsyncTask(title, taskInstance(monitor), null); + } + + + @Override + public void runSyncWithProgress(ProgressRunnable progressRunnable) { + String title = this.getClass().getSimpleName() + referenceCount++; + progressView.startSyncTask(title, taskInstance(progressRunnable)); + } + + + @Override + public void runSyncWithoutProgress(Runnable runnable) { + runnable.run(); + } + + private ITask taskInstance(ProgressRunnable progressRunnable) + { + return new ITask(){ + + @Override + public void run(ITaskMonitor monitor) { + ProgressIndicator progressIndicator = new SdkProgressIndicator(monitor); + ProgressRunner progressRunner = new ProgressRunner(){ + + @Override + public void runAsyncWithProgress(ProgressRunnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runAsyncWithProgress(r); + } + + @Override + public void runSyncWithProgress(ProgressRunnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runSyncWithProgress(r); + } + + @Override + public void runSyncWithoutProgress(Runnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runSyncWithoutProgress(r); + }}; + progressRunnable.run(progressIndicator, progressRunner); + } + }; + + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java new file mode 100644 index 00000000..a4066caf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java @@ -0,0 +1,1125 @@ +/* + * 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.sdkuilib.internal.repository.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +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.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.repository.api.License; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepositorySource; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.util.InstallerUtil; +import com.android.sdklib.AndroidVersion; +import com.android.sdkuilib.internal.repository.PackageInfo; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser; +import org.eclipse.andmore.base.resources.ImageFactory; +import com.android.sdkuilib.ui.GridDialog; + + +/** + * Implements an {@link SdkUpdaterChooserDialog}. + */ +public final class SdkUpdaterChooserDialog extends GridDialog { + + /** Last dialog size for this session. */ + private static Point sLastSize; + /** Precomputed flag indicating whether the "accept license" radio is checked. */ + private boolean mAcceptSameAllLicense; + private boolean mInternalLicenseRadioUpdate; + + // UI fields + private SashForm mSashForm; + private Composite mPackageRootComposite; + private TreeViewer mTreeViewPackage; + private Tree mTreePackage; + private TreeColumn mTreeColum; + private StyledText mPackageText; + private Button mLicenseRadioAccept; + private Button mLicenseRadioReject; + private Button mLicenseRadioAcceptLicense; + private Group mPackageTextGroup; + private Group mTableGroup; + private Label mErrorLabel; + private Set unacceptedLicenses = new HashSet<>(); + + private final SdkContext mSdkContext; + /** + * List of all archives to be installed with dependency information. + *

+ * Note: in a lot of cases, we need to find the archive info for a given archive. This + * is currently done using a simple linear search, which is fine since we only have a very + * limited number of archives to deal with (e.g. < 10 now). We might want to revisit + * this later if it becomes an issue. Right now just do the simple thing. + *

+ * Typically we could add a map Package=>PackageInfo later. + */ + private final List mPackages = new ArrayList<>(); + + + + /** + * Create the dialog. + * + * @param parentShell The shell to use, typically updaterData.getWindowShell() + * @param SdkContext The updater data + * @param packages The packages to be installed + */ + public SdkUpdaterChooserDialog(Shell parentShell, + SdkContext SdkContext, + Collection updates, + List newPackages) { + super(parentShell, 3, false/*makeColumnsEqual*/); + mSdkContext = SdkContext; + init(updates); + init(newPackages); + } + + @Override + protected boolean isResizable() { + return true; + } + + /** + * Returns the results, i.e. the list of selected new packages to install. + *

+ * An empty list is returned if cancel was chosen. + */ + public ArrayList getResult() { + ArrayList packageList = new ArrayList(); + + if (getReturnCode() == Window.OK) { + for (PackageInfo packageInfo : mPackages) { + if (packageInfo.isAccepted()) { + packageList.add(packageInfo.getNewPackage()); + } + } + } + return packageList; + } + + /** + * Create the main content of the dialog. + * See also {@link #createButtonBar(Composite)} below. + */ + @Override + public void createDialogContent(Composite parent) { + // Sash form + mSashForm = new SashForm(parent, SWT.NONE); + mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1)); + + // Left part of Sash Form + + mTableGroup = new Group(mSashForm, SWT.NONE); + mTableGroup.setText("Packages"); + mTableGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); + + mTreeViewPackage = new TreeViewer(mTableGroup, SWT.BORDER | SWT.V_SCROLL | SWT.SINGLE); + mTreePackage = mTreeViewPackage.getTree(); + mTreePackage.setHeaderVisible(false); + mTreePackage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mTreePackage.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + onPackageSelected(); //$hide$ + } + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onPackageDoubleClick(); + } + }); + + mTreeColum = new TreeColumn(mTreePackage, SWT.NONE); + mTreeColum.setWidth(100); + mTreeColum.setText("Packages"); + + // Right part of Sash form + + mPackageRootComposite = new Composite(mSashForm, SWT.NONE); + mPackageRootComposite.setLayout(new GridLayout(4, false/*makeColumnsEqual*/)); + mPackageRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + mPackageTextGroup = new Group(mPackageRootComposite, SWT.NONE); + mPackageTextGroup.setText("Package Description && License"); + mPackageTextGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1)); + mPackageTextGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); + + mPackageText = new StyledText(mPackageTextGroup, + SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + mPackageText.setBackground( + getParentShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); + mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mLicenseRadioAccept = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioAccept.setText("Accept"); + mLicenseRadioAccept.setToolTipText("Accept this package."); + mLicenseRadioAccept.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + mLicenseRadioReject = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioReject.setText("Reject"); + mLicenseRadioReject.setToolTipText("Reject this package."); + mLicenseRadioReject.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + Link link = new Link(mPackageRootComposite, SWT.NONE); + link.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1)); + final String printAction = "Print"; // extracted for NLS, to compare with below. + link.setText(String.format("Copy to clipboard | %1$s", printAction)); + link.setToolTipText("Copies all text and license to clipboard | Print using system defaults."); + link.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + if (printAction.equals(e.text)) { + mPackageText.print(); + } else { + Point p = mPackageText.getSelection(); + mPackageText.selectAll(); + mPackageText.copy(); + mPackageText.setSelection(p); + } + } + }); + + + mLicenseRadioAcceptLicense = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioAcceptLicense.setText("Accept License"); + mLicenseRadioAcceptLicense.setToolTipText("Accept all packages that use the same license."); + mLicenseRadioAcceptLicense.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + mSashForm.setWeights(new int[] {200, 300}); + } + + /** + * Creates and returns the contents of this dialog's button bar. + *

+ * This reimplements most of the code from the base class with a few exceptions: + *

    + *
  • Enforces 3 columns. + *
  • Inserts a full-width error label. + *
  • Inserts a help label on the left of the first button. + *
  • Renames the OK button into "Install" + *
+ */ + @Override + protected Control createButtonBar(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 0; // this is incremented by createButton + layout.makeColumnsEqualWidth = false; + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + composite.setLayout(layout); + GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1); + composite.setLayoutData(data); + composite.setFont(parent.getFont()); + + // Error message area + mErrorLabel = new Label(composite, SWT.NONE); + mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1)); + + // Label at the left of the install/cancel buttons + Label label = new Label(composite, SWT.NONE); + label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + label.setText("[*] Something depends on this package"); + label.setEnabled(false); + layout.numColumns++; + + // Add the ok/cancel to the button bar. + createButtonsForButtonBar(composite); + + // the ok button should be an "install" button + Button button = getButton(IDialogConstants.OK_ID); + button.setText("Install"); + + return composite; + } + + // -- End of UI, Start of internal logic ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + @Override + public void create() { + super.create(); + // set window title + getShell().setText("Choose Packages to Install"); + setWindowImage(); + // Automatically accept those with no license + for (PackageInfo packageInfo : mPackages) { + RemotePackage remote = packageInfo.getNewPackage(); + License license = remote.getLicense(); + if (license == null) + packageInfo.setAccepted(true); + else { + boolean hasLicense = license.checkAccepted(mSdkContext.getLocation(), mSdkContext.getFileOp()); + if (hasLicense) + packageInfo.setAccepted(true); + else + unacceptedLicenses.add(remote); + } + } + // Fill the list with the replacement packages + mTreeViewPackage.setLabelProvider(new NewPackagesLabelProvider()); + mTreeViewPackage.setContentProvider(new NewPackagesContentProvider()); + mTreeViewPackage.setInput(createTreeInput(mPackages)); + mTreeViewPackage.expandAll(); + adjustColumnsWidth(); + // select first item is superfluous + //onPackageSelected(contents.get(0)); + } + + /** + * Creates the icon of the window shell. + */ + private void setWindowImage() { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + getShell().setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth() { + // Add a listener to resize the column to the full width of the table + ControlAdapter resizer = new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = mTreePackage.getClientArea(); + mTreeColum.setWidth(r.width); + } + }; + mTreePackage.addControlListener(resizer); + resizer.controlResized(null); + } + + /** + * Captures the window size before closing this. + * @see #getInitialSize() + */ + @Override + public boolean close() { + sLastSize = getShell().getSize(); + return super.close(); + } + + /** + * Tries to reuse the last window size during this session. + *

+ * Note: the alternative would be to implement {@link #getDialogBoundsSettings()} + * since the default {@link #getDialogBoundsStrategy()} is to persist both location + * and size. + */ + @Override + protected Point getInitialSize() { + if (sLastSize != null) { + return sLastSize; + } else { + // Arbitrary values that look good on my screen and fit on 800x600 + return new Point(740, 470); + } + } + + /** + * Callback invoked when a package item is selected in the list. + */ + private void onPackageSelected() { + Object item = getSelectedItem(); + + // Update mAcceptSameAllLicense : true if all items under the same license are accepted. + PackageInfo packageInfo = null; + List list = null; + if (item instanceof PackageInfo) { + packageInfo = (PackageInfo) item; + + Object p = + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()).getParent(packageInfo); + if (p instanceof LicenseEntry) { + list = ((LicenseEntry) p).getPackageInfoList(); + } + displayPackageInformation(packageInfo); + + } else if (item instanceof LicenseEntry) { + LicenseEntry entry = (LicenseEntry) item; + list = entry.getPackageInfoList(); + displayLicenseInformation(entry); + + } else { + // Fallback, should not happen. + displayEmptyInformation(); + } + + // the "Accept License" radio is selected if there's a license with >= 0 items + // and they are all in "accepted" state. + mAcceptSameAllLicense = list != null && list.size() > 0; + if (mAcceptSameAllLicense) { + assert list != null; + License lic0 = list.get(0).getNewPackage().getLicense(); + for (PackageInfo packageInfo2 : list) { + License lic2 = packageInfo2.getNewPackage().getLicense(); + if (packageInfo2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) { + continue; + } else { + mAcceptSameAllLicense = false; + break; + } + } + } + + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); + } + + /** Returns the currently selected tree item. + * @return Either {@link PackageInfo} or {@link LicenseEntry} or null. */ + private Object getSelectedItem() { + ISelection sel = mTreeViewPackage.getSelection(); + if (sel instanceof IStructuredSelection) { + Object elem = ((IStructuredSelection) sel).getFirstElement(); + if (elem instanceof PackageInfo || elem instanceof LicenseEntry) { + return elem; + } + } + return null; + } + + /** + * Information displayed when nothing valid is selected. + */ + private void displayEmptyInformation() { + mPackageText.setText("Please select a package or a license."); + } + + /** + * Updates the package description and license text depending on the selected package. + *

+ * Note that right now there is no logic to support more than one level of dependencies + * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends + * solely on B's state). We currently don't need this. It would be straightforward to add + * if we had a need for it, though. This would require changes to {@link PackageInfo} and + * {@link SdkUpdaterLogic}. + */ + private void displayPackageInformation(PackageInfo packageInfo) { + mPackageText.setText(""); //$NON-NLS-1$ + addSectionTitle("Package Description\n"); + RemotePackage remotePackage = packageInfo.getNewPackage(); + // Add revision if not in path + addText(getName(remotePackage), "\n\n"); //$NON-NLS-1$ + + LocalPackage localPackage = packageInfo.getReplaced(); + if (localPackage != null) { + AndroidVersion vOld = PackageAnalyser.getAndroidVersion(localPackage); + AndroidVersion vNew = PackageAnalyser.getAndroidVersion(remotePackage); + boolean showRev = (vOld != null) && (vNew != null); + + if (showRev && !vOld.equals(vNew)) { + // Versions are different, so indicate more than just the revision. + addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n", + vOld.getApiString(), localPackage.getVersion(), + vNew.getApiString(), remotePackage.getVersion())); + showRev = false; + } + if (showRev) { + addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n", + localPackage.getVersion(), + remotePackage.getVersion())); + } + } + List required = InstallerUtil.computeRequiredPackages( + Collections.singletonList(remotePackage), mSdkContext.getPackages(), + mSdkContext.getProgressIndicator()); + if ((required != null && required.size() > 0)) { + // Remove principal and duplicates + Iterator iterator = required.iterator(); + Set existenceSet = new HashSet<>(); + existenceSet.add(remotePackage); + List filteredRequired = new ArrayList<>(); + while (iterator.hasNext()) { + RemotePackage requiredPackage = iterator.next(); + if (!existenceSet.contains(requiredPackage)) { + { + existenceSet.add(requiredPackage); + filteredRequired.add(requiredPackage); + } + } + } + // Remove references now existenceSet no longer needed + existenceSet.clear(); + if ((filteredRequired.size() > 0)) { + addSectionTitle("Dependencies\n"); + addText("Installing this package also requires installing:"); + for (RemotePackage dependency : filteredRequired) { + addText(String.format("\n- %1$s", getName(dependency))); + } + addText("\n\n"); + /* + if (ai.isDependencyFor()) { + addText("This package is a dependency for:"); + for (PackageInfo ai2 : ai.getDependenciesFor()) { + addText(String.format("\n- %1$s", + ai2.getShortDescription())); + } + addText("\n\n"); + } + */ + } + } + + addSectionTitle("Archive Size\n"); + long fileSize = remotePackage.getArchive().getComplete().getSize(); + addText(Utilities.formatFileSize(fileSize), "\n\n"); //$NON-NLS-1$ + + License license = remotePackage.getLicense(); + if (license != null) { + addSectionTitle(String.format("License %s:%n", license.getId())); + addText(license.getValue().trim(), "\n\n"); //$NON-NLS-1$ + } + + addSectionTitle("Site\n"); + RepositorySource source = remotePackage.getSource(); + if (source != null) { + addText(source.getDisplayName()); + } + } + + /** + * Updates the description for a license entry. + */ + private void displayLicenseInformation(LicenseEntry entry) { + List packageInfoList = entry == null ? null : entry.getPackageInfoList(); + if (packageInfoList == null) { + // There should not be a license entry without any package in it. + displayEmptyInformation(); + return; + } + assert entry != null; + + mPackageText.setText(""); //$NON-NLS-1$ + + License license = null; + addSectionTitle("Packages\n"); + for (PackageInfo packageInfo : entry.getPackageInfoList()) { + RemotePackage remote = packageInfo.getNewPackage(); + if (license == null) + license = remote.getLicense(); + addText("- ", remote.getDisplayName(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + if (license != null) { + addSectionTitle(String.format("License %s:%n", license.getId())); + addText(license.getValue().trim(), "\n\n"); //$NON-NLS-1$ + } + } + + private void addText(String...string) { + for (String s : string) { + mPackageText.append(s); + } + } + + private void addSectionTitle(String string) { + String s = mPackageText.getText(); + int start = (s == null ? 0 : s.length()); + mPackageText.append(string); + + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = string.length(); + sr.fontStyle = SWT.BOLD; + sr.underline = true; + mPackageText.setStyleRange(sr); + } + + private void updateLicenceRadios(PackageInfo ai) { + if (mInternalLicenseRadioUpdate) { + return; + } + mInternalLicenseRadioUpdate = true; + + boolean oneAccepted = false; + + mLicenseRadioAcceptLicense.setSelection(mAcceptSameAllLicense); + oneAccepted = ai != null && ai.isAccepted(); + mLicenseRadioAccept.setEnabled(ai != null); + mLicenseRadioReject.setEnabled(ai != null); + mLicenseRadioAccept.setSelection(oneAccepted); + mLicenseRadioReject.setSelection(ai != null && ai.isRejected()); + + // The install button is enabled if there's at least one package accepted. + // If the current one isn't, look for another one. + boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0; + if (!missing && !oneAccepted) { + for(PackageInfo ai2 : mPackages) { + if (ai2.isAccepted()) { + oneAccepted = true; + break; + } + } + } + getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted); + mInternalLicenseRadioUpdate = false; + } + + /** + * Callback invoked when one of the radio license buttons is selected. + * + * - accept/refuse: toggle, update item checkbox + * - accept all: set accept-all, check all items with the *same* license + */ + private void onLicenseRadioSelected() { + if (mInternalLicenseRadioUpdate) { + return; + } + mInternalLicenseRadioUpdate = true; + + Object item = getSelectedItem(); + PackageInfo packageInfo = (item instanceof PackageInfo) ? (PackageInfo) item : null; + boolean needUpdate = true; + + if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) { + // Accept all has been switched on. Mark all packages as accepted + + List list = null; + if (item instanceof LicenseEntry) { + list = ((LicenseEntry) item).getPackageInfoList(); + } else if (packageInfo != null) { + Object p = ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()) + .getParent(packageInfo); + if (p instanceof LicenseEntry) { + list = ((LicenseEntry) p).getPackageInfoList(); + } + } + + if (list != null && list.size() > 0) { + mAcceptSameAllLicense = true; + for(PackageInfo packageInfo2 : list) { + packageInfo2.setAccepted(true); + packageInfo2.setRejected(false); + unacceptedLicenses.remove(packageInfo2.getNewPackage()); + } + } + + } else if (packageInfo != null && mLicenseRadioAccept.getSelection()) { + // Accept only this one + mAcceptSameAllLicense = false; + packageInfo.setAccepted(true); + packageInfo.setRejected(false); + unacceptedLicenses.remove(packageInfo.getNewPackage()); + } else if (packageInfo != null && mLicenseRadioReject.getSelection()) { + // Reject only this one + mAcceptSameAllLicense = false; + packageInfo.setAccepted(false); + packageInfo.setRejected(true); + unacceptedLicenses.add(packageInfo.getNewPackage()); + } else { + needUpdate = false; + } + + mInternalLicenseRadioUpdate = false; + + if (needUpdate) { + if (mAcceptSameAllLicense) { + mTreeViewPackage.refresh(); + } else { + mTreeViewPackage.refresh(packageInfo); + mTreeViewPackage.refresh( + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(packageInfo)); + } + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); + } + } + + /** + * Callback invoked when a package item is double-clicked in the list. + */ + private void onPackageDoubleClick() { + Object item = getSelectedItem(); + + if (item instanceof PackageInfo) { + PackageInfo packageInfo = (PackageInfo) item; + boolean wasAccepted = packageInfo.isAccepted(); + packageInfo.setAccepted(!wasAccepted); + packageInfo.setRejected(wasAccepted); + + // update state + mAcceptSameAllLicense = false; + mTreeViewPackage.refresh(packageInfo); + // refresh parent since its icon might have changed. + mTreeViewPackage.refresh( + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(packageInfo)); + + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); + + } else if (item instanceof LicenseEntry) { + mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item)); + } + } + + /** + * Computes and displays missing dependencies. + * + * If there's a selected package, check the dependency for that one. + * Otherwise display the first missing dependency of any other package. + */ + private void displayMissingDependency(PackageInfo packageInfo) { + String error = null; + + try { + if (packageInfo != null) { + if (packageInfo.isAccepted()) { + // Case where this package is accepted but blocked by another non-accepted one + List required = InstallerUtil.computeRequiredPackages( + Collections.singletonList(packageInfo.getNewPackage()), mSdkContext.getPackages(), + mSdkContext.getProgressIndicator()); + if ((required != null && required.size() > 0)) { + for (RemotePackage dependency : required) { + if (unacceptedLicenses.contains(dependency)) { + error = String.format("This package depends on '%1$s'.", + dependency.getDisplayName()); + return; + } + } + } + } else { + /* + // Case where this package blocks another one when not accepted + for (PackageInfo adep : packageInfo.getDependenciesFor()) { + // It only matters if the blocked one is accepted + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on this one.", + adep.getShortDescription()); + return; + } + } + */ + } + } +/* + // If there is no missing dependency on the current selection, + // just find the first missing dependency of any other package. + for (PackageInfo packageInfo2 : mArchives) { + if (packageInfo2 == packageInfo) { + // We already processed that one above. + continue; + } + if (packageInfo2.isAccepted()) { + // The user requested to install this package. + // Check if all its dependencies are met. + PackageInfo[] adeps = packageInfo2.getDependsOn(); + if (adeps != null) { + for (PackageInfo adep : adeps) { + if (!adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + packageInfo2.getShortDescription(), + adep.getShortDescription()); + return; + } + } + } + } else { + // The user did not request to install this package. + // Check whether this package blocks another one when not accepted. + for (PackageInfo adep : packageInfo2.getDependenciesFor()) { + // It only matters if the blocked one is accepted + // or if it's a local archive that is already installed (these + // are marked as implicitly accepted, so it's the same test.) + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + adep.getShortDescription(), + packageInfo2.getShortDescription()); + return; + } + } + } + } +*/ + } finally { + mErrorLabel.setText(error == null ? "" : error); //$NON-NLS-1$ + } + } + + + /** + * Provides the labels for the tree view. + * Root branches are {@link LicenseEntry} elements. + * Leave nodes are {@link PackageInfo} which all have the same license. + */ + private class NewPackagesLabelProvider extends LabelProvider { + @Override + public Image getImage(Object element) { + if (element instanceof PackageInfo) { + // Package icon: accepted (green), rejected (red), not set yet (question mark) + PackageInfo packageInfo = (PackageInfo) element; + + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + if (packageInfo.isAccepted()) { + return imgFactory.getImageByName("accept_icon16.png"); + } else if (packageInfo.isRejected()) { + return imgFactory.getImageByName("reject_icon16.png"); + } + return imgFactory.getImageByName("unknown_icon16.png"); + } + return super.getImage(element); + + } else if (element instanceof LicenseEntry) { + // License icon: green if all below are accepted, red if all rejected, otherwise + // no icon. + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + boolean allAccepted = true; + boolean allRejected = true; + for (PackageInfo packageInfo : ((LicenseEntry) element).getPackageInfoList()) { + allAccepted = allAccepted && packageInfo.isAccepted(); + allRejected = allRejected && packageInfo.isRejected(); + } + if (allAccepted && !allRejected) { + return imgFactory.getImageByName("accept_icon16.png"); + } else if (!allAccepted && allRejected) { + return imgFactory.getImageByName("reject_icon16.png"); + } + } + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof LicenseEntry) { + return ((LicenseEntry) element).getLicenseRef(); + + } else if (element instanceof PackageInfo) { + PackageInfo packageInfo = (PackageInfo) element; + + String desc = packageInfo.getNewPackage().getDisplayName(); + + // if (packageInfo.isDependencyFor()) { + // desc += " [*]"; + // } + return desc; + } + assert element instanceof String || element instanceof PackageInfo; + return null; + } + } + + /** + * Provides the content for the tree view. + * Root branches are {@link LicenseEntry} elements. + * Leave nodes are {@link PackageInfo} which all have the same license. + */ + private class NewPackagesContentProvider implements ITreeContentProvider { + private List mInput; + + @Override + public void dispose() { + // pass + } + + @SuppressWarnings("unchecked") + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // Input should be the result from createTreeInput. + if (newInput instanceof List && + ((List) newInput).size() > 0 && + ((List) newInput).get(0) instanceof LicenseEntry) { + mInput = (List) newInput; + } else { + mInput = null; + } + } + + @Override + public boolean hasChildren(Object parent) { + if (parent instanceof List) { + // This is the root of the tree. + return true; + + } else if (parent instanceof LicenseEntry) { + return ((LicenseEntry) parent).getPackageInfoList().size() > 0; + } + + return false; + } + + @Override + public Object[] getElements(Object parent) { + return getChildren(parent); + } + + @Override + public Object[] getChildren(Object parent) { + if (parent instanceof List) { + return ((List) parent).toArray(); + + } else if (parent instanceof LicenseEntry) { + return ((LicenseEntry) parent).getPackageInfoList().toArray(); + } + + return new Object[0]; + } + + @Override + public Object getParent(Object child) { + if (child instanceof LicenseEntry) { + return ((LicenseEntry) child).getRoot(); + + } else if (child instanceof PackageInfo && mInput != null) { + for (LicenseEntry entry : mInput) { + if (entry.getPackageInfoList().contains(child)) { + return entry; + } + } + } + + return null; + } + } + + /** + * Represents a branch in the view tree: an entry where all the sub-archive info + * share the same license. Contains a link to the share root list for convenience. + */ + private static class LicenseEntry { + private final List mRoot; + private final String mLicenseRef; + private final List mPackageInfoList; + + public LicenseEntry( + @NonNull List root, + @NonNull String licenseRef, + @NonNull List packageInfoList) { + mRoot = root; + mLicenseRef = licenseRef; + mPackageInfoList = packageInfoList; + } + + @NonNull + public List getRoot() { + return mRoot; + } + + @NonNull + public String getLicenseRef() { + return mLicenseRef; + } + + @NonNull + public List getPackageInfoList() { + return mPackageInfoList; + } + } + + /** + * Creates the tree structure based on the given archives. + * The current structure is to have a branch per license type, + * with all the archives sharing the same license under it. + * Elements with no license are left at the root. + * + * @param archives The non-null collection of archive info to display. Ideally non-empty. + * @return A list of {@link LicenseEntry}, each containing a list of {@link PackageInfo}. + */ + @NonNull + private List createTreeInput(@NonNull List packageInfoList) { + // Build an ordered map with all the licenses, ordered by license ref name. + final String noLicense = "No license"; //NLS + + Comparator comp = new Comparator() { + @Override + public int compare(String s1, String s2) { + boolean first1 = noLicense.equals(s1); + boolean first2 = noLicense.equals(s2); + if (first1 && first2) { + return 0; + } else if (first1) { + return -1; + } else if (first2) { + return 1; + } + return s1.compareTo(s2); + } + }; + + Map> packageInfoMap = new TreeMap>(comp); + + for (PackageInfo info : packageInfoList) { + String ref = noLicense; + License license = info.getNewPackage().getLicense(); + if (license != null) { + ref = license.getId(); //prettyLicenseRef(license.getLicenseRef()); + } + + List list = packageInfoMap.get(ref); + if (list == null) { + packageInfoMap.put(ref, list = new ArrayList()); + } + list.add(info); + } + + // Transform result into a list + List licensesList = new ArrayList(); + for (Map.Entry> entry : packageInfoMap.entrySet()) { + licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue())); + } + return licensesList; + } + + private void init(List newPackages) { + Iterator iterator = newPackages.iterator(); + while(iterator.hasNext()) + mPackages.add(new PackageInfo(iterator.next())); + } + + private void init(Collection updates) { + Iterator iterator = updates.iterator(); + while(iterator.hasNext()) { + UpdatablePackage updatable = iterator.next(); + mPackages.add(new PackageInfo(updatable.getRemote(), updatable.getLocal())); + } + } + + private String getName(RemotePackage remotePackage) { + // Add revision if not in path + String path = remotePackage.getPath(); + String version = null; + int index = path.lastIndexOf(';'); + if (index != -1) { + version = path.substring(index + 1); + if (!Character.isDigit(version.charAt(0))) + version = null; + } + String packageName = remotePackage.getDisplayName(); + if (version == null) + packageName = packageName + " " + remotePackage.getVersion().toShortString(); + return packageName; + } + + /** + * Reformats the licenseRef to be more human-readable. + * It's an XML ref and in practice it looks like [oem-]android-[type]-license. + * If it's not a format we can deal with, leave it alone. + */ + /* + private String prettyLicenseRef(String ref) { + // capitalize every word + StringBuilder sb = new StringBuilder(); + boolean capitalize = true; + for (char c : ref.toCharArray()) { + if (c >= 'a' && c <= 'z') { + if (capitalize) { + c = (char) (c + 'A' - 'a'); + capitalize = false; + } + } else { + if (c == '-') { + c = ' '; + } + capitalize = true; + } + sb.append(c); + } + + ref = sb.toString(); + + // A few acronyms should stay upper-case + for (String w : new String[] { "Sdk", "Mips", "Arm" }) { + ref = ref.replaceAll(w, w.toUpperCase(Locale.US)); + } + + return ref; + } +*/ + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java new file mode 100644 index 00000000..0aab88ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java @@ -0,0 +1,544 @@ +/* + * 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.sdkuilib.internal.repository.ui; + + +import com.android.SdkConstants; +import com.android.sdkuilib.internal.repository.AboutDialog; +import com.android.sdkuilib.internal.repository.MenuBarWrapper; +import com.android.sdkuilib.internal.repository.Settings; +import com.android.sdkuilib.internal.repository.SettingsDialog; +import com.android.sdkuilib.internal.repository.content.PackageType; + +import org.eclipse.andmore.base.resources.ImageFactory; +import com.android.sdkuilib.internal.repository.ui.PackagesPage.MenuAction; +import com.android.sdkuilib.internal.widgets.ImgDisabledButton; +import com.android.sdkuilib.internal.widgets.ToggleButton; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; +import com.android.utils.ILogger; + +import java.io.File; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DirectoryDialog; +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.ProgressBar; +import org.eclipse.swt.widgets.Shell; + +/** + * This is the private implementation of the UpdateWindow + * for the second version of the SDK Manager. + *

+ * This window features only one embedded page, the combined installed+available package list. + */ +public class SdkUpdaterWindowImpl2 { + + public static final String APP_NAME = "Android SDK Manager"; + private static final String SIZE_POS_PREFIX = "sdkman2"; //$NON-NLS-1$ + private static final String ANDROID_SDK_LOCATION_PROMPT = "Android SDK Location" ; + + private final Shell mParentShell; + private final SdkInvocationContext mContext; + private final SdkContext mSdkContext; + + // --- UI members --- + + protected Shell mShell; + private PackagesPage mPkgPage; + private ProgressBar mProgressBar; + private Label mStatusText; + private ImgDisabledButton mButtonStop; + private ToggleButton mButtonShowLog; + private LogWindow mLogWindow; + /** Set of package types on which to filter. An empty set indicates no filtering */ + private Set packageTypeSet; + + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param sdkContext SDK handler and repo manager + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindowImpl2( + Shell parentShell, + SdkContext sdkContext, + SdkInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSdkContext = sdkContext; + } + + public void addPackageFilter(PackageType packageType) { + if (packageTypeSet == null) + packageTypeSet = new TreeSet(); + packageTypeSet.add(packageType); + } + + /** + * Opens the window. + * @wbp.parser.entryPoint + */ + public void open() { + if (mParentShell == null) { + Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer) + } + + createShell(); + File sdkLocation = mSdkContext.getLocation(); + boolean sdkLocationExists = sdkLocation.exists(); + boolean sdkLocationIsDir = sdkLocation.isDirectory(); + if (!sdkLocationExists) { + mSdkContext.getSdkLog().error(null, "Android SDK directory not found: %s", sdkLocation.getPath()); + sdkLocation = promptForSdkLocation(ANDROID_SDK_LOCATION_PROMPT) ; + } else if (!sdkLocationIsDir) { + mSdkContext.getSdkLog().error(null, "Android SDK location is not a directory: %s", sdkLocation.getPath()); + sdkLocation = promptForSdkLocation(ANDROID_SDK_LOCATION_PROMPT); + } + if (sdkLocation == null) + return; + if (!sdkLocationExists || !sdkLocationIsDir) { + // TODO - Persist location + mSdkContext.setLocation(sdkLocation); + } + preCreateContent(); + createContents(); + createMenuBar(); + createLogWindow(); + mShell.open(); + mShell.layout(); + + if (postCreateContent()) { //$hide$ (hide from SWT designer) + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } + dispose(); //$hide$ + } + + private void createShell() { + // The SDK Manager must use a shell trim when standalone + // or a dialog trim when invoked from somewhere else. + int style = SWT.SHELL_TRIM; + if (mContext != SdkInvocationContext.STANDALONE) { + style |= SWT.APPLICATION_MODAL; + } + + mShell = new Shell(mParentShell, style); + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX); + } + }); + + GridLayout glShell = new GridLayout(2, false); + glShell.verticalSpacing = 0; + glShell.horizontalSpacing = 0; + glShell.marginWidth = 0; + glShell.marginHeight = 0; + mShell.setLayout(glShell); + + mShell.setMinimumSize(new Point(600, 300)); + mShell.setSize(700, 500); + mShell.setText(APP_NAME); + + ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX); + } + + private File promptForSdkLocation(String title) { + DirectoryDialog dialog = new DirectoryDialog (mShell); + dialog.setText(title); + String platform = SWT.getPlatform(); + dialog.setFilterPath (platform.equals("win32") ? "c:\\" : "/"); + String sdkLocation = dialog.open (); + if (sdkLocation != null) + return new File(sdkLocation); + return null; + } + + private void createContents() { + mPkgPage = new PackagesPage(mShell, SWT.NONE, mSdkContext, mContext, packageTypeSet); + mPkgPage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + + Composite composite1 = new Composite(mShell, SWT.NONE); + composite1.setLayout(new GridLayout(1, false)); + composite1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mProgressBar = new ProgressBar(composite1, SWT.NONE); + mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mStatusText = new Label(composite1, SWT.NONE); + mStatusText.setText("Status Placeholder"); //$NON-NLS-1$ placeholder + mStatusText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + Composite composite2 = new Composite(mShell, SWT.NONE); + composite2.setLayout(new GridLayout(2, false)); + + mButtonStop = new ImgDisabledButton(composite2, SWT.NONE, + getImage("stop_enabled_16.png"), //$NON-NLS-1$ + getImage("stop_disabled_16.png"), //$NON-NLS-1$ + "Click to abort the current task", + ""); //$NON-NLS-1$ nothing to abort + mButtonStop.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + onStopSelected(); + } + }); + + mButtonShowLog = new ToggleButton(composite2, SWT.NONE, + getImage("log_off_16.png"), //$NON-NLS-1$ + getImage("log_on_16.png"), //$NON-NLS-1$ + "Click to show the log window", // tooltip for state hidden=>shown + "Click to hide the log window"); // tooltip for state shown=>hidden + mButtonShowLog.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + onToggleLogWindow(); + } + }); + } + + private void createMenuBar() { + + Menu menuBar = new Menu(mShell, SWT.BAR); + mShell.setMenuBar(menuBar); + + MenuItem menuBarPackages = new MenuItem(menuBar, SWT.CASCADE); + menuBarPackages.setText("Packages"); + + Menu menuPkgs = new Menu(menuBarPackages); + menuBarPackages.setMenu(menuPkgs); + MenuItem showUpdatesNew = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_NEW_PKG.getMenuStyle()); + showUpdatesNew.setText( + MenuAction.TOGGLE_SHOW_NEW_PKG.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_NEW_PKG, showUpdatesNew); + + //MenuItem showInstalled = new MenuItem(menuPkgs, + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuStyle()); + //showInstalled.setText( + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuTitle()); + //mPkgPage.registerMenuAction( + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG, showInstalled); + + MenuItem showObsoletePackages = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuStyle()); + showObsoletePackages.setText( + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG, showObsoletePackages); + + new MenuItem(menuPkgs, SWT.SEPARATOR); + + MenuItem reload = new MenuItem(menuPkgs, + MenuAction.RELOAD.getMenuStyle()); + reload.setText( + MenuAction.RELOAD.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.RELOAD, reload); + MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE); + menuBarTools.setText("Tools"); + + Menu menuTools = new Menu(menuBarTools); + menuBarTools.setMenu(menuTools); + + if (mContext == SdkInvocationContext.STANDALONE) { + MenuItem manageAvds = new MenuItem(menuTools, SWT.NONE); + manageAvds.setText("Manage AVDs..."); + manageAvds.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + onAvdManager(); + } + }); + } + if (mContext == SdkInvocationContext.STANDALONE || mContext == SdkInvocationContext.IDE) { + try { + new MenuBarWrapper(APP_NAME, menuTools) { + @Override + public void onPreferencesMenuSelected() { + + // capture a copy of the initial settings + Settings settings1 = mSdkContext.getSettings().copy(); + + // open the dialog and wait for it to close + SettingsDialog sd = new SettingsDialog(mShell, mSdkContext); + sd.open(); + + // get the new settings + Settings settings2 = mSdkContext.getSettings(); + + // We need to reload the package list if the http mode or the preview + // modes have changed. + if (settings1.getForceHttp() != settings2.getForceHttp() || + settings1.getEnablePreviews() != settings2.getEnablePreviews()) { + mPkgPage.onSdkReload(); + } + } + + @Override + public void onAboutMenuSelected() { + AboutDialog ad = new AboutDialog(mShell, mSdkContext); + ad.open(); + } + + @Override + public void printError(String format, Object... args) { + if (mSdkContext != null) { + mSdkContext.getSdkLog().error(null, format, args); + } + } + }; + } catch (Throwable e) { + mSdkContext.getSdkLog().error(e, "Failed to setup menu bar"); + e.printStackTrace(); + } + } + } + + private Image getImage(String filename) { + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + return imgFactory.getImageByName(filename); + } + } + return null; + } + + /** + * Creates the log window. + *

+ * If this is invoked from an IDE, we also define a secondary logger so that all + * messages flow to the IDE log. This may or may not be what we want in the end + * (e.g. a middle ground would be to repeat error, and ignore normal/verbose) + */ + private void createLogWindow() { + mLogWindow = new LogWindow(mShell, + mContext == SdkInvocationContext.IDE ? mSdkContext.getSdkLog() : null); + mLogWindow.open(); + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // --- Public API ----------- + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + */ + public void addListener(ISdkChangeListener listener) { + mSdkContext.getSdkHelper().addListeners(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public void removeListener(ISdkChangeListener listener) { + mSdkContext.getSdkHelper().removeListener(listener); + } + + // --- Internals & UI Callbacks ----------- + + /** + * Called before the UI is created. + */ + private void preCreateContent() { + mSdkContext.getSdkHelper().setWindowShell(mShell); + // Note: we can't create the TaskFactory yet because we need the UI + // to be created first, so this is done in postCreateContent(). + } + + /** + * Once the UI has been created, initializes the content. + * This creates the pages, selects the first one, setups sources and scans for local folders. + * + * Returns true if we should show the window. + */ + private boolean postCreateContent() { + + // This class delegates all logging to the mLogWindow window + // and filters errors to make sure the window is visible when + // an error is logged. + SdkProgressFactory.ISdkLogWindow logAdapter = new SdkProgressFactory.ISdkLogWindow() { + @Override + public void setDescription(String description) { + mLogWindow.setDescription(description); + } + + @Override + public void log(String log) { + mLogWindow.log(log); + } + + @Override + public void logVerbose(String log) { + mLogWindow.logVerbose(log); + } + + @Override + public void logError(String log) { + mLogWindow.logError(log); + } + @Override + public void show() + { + // Run the window visibility check/toggle on the UI thread. + // Note: at least on Windows, it seems ok to check for the window visibility + // on a sub-thread but that doesn't seem cross-platform safe. We shouldn't + // have a lot of error logging, so this should be acceptable. If not, we could + // cache the visibility state. + if (mShell != null && !mShell.isDisposed()) { + mShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mLogWindow.isVisible()) { + // Don't toggle the window visibility directly. + // Instead use the same action as the log-toggle button + // so that the button's state be kept in sync. + onToggleLogWindow(); + } + } + }); + } + } + }; + SdkProgressFactory factory = new SdkProgressFactory(mStatusText, mProgressBar, mButtonStop, logAdapter); + setWindowImage(mShell); + if (!initializeSettings()) + return false; + if (mSdkContext.hasError()) + { + ILogger logger = (ILogger)factory; + Iterator iterator = mSdkContext.getLogMessages().iterator(); + while(iterator.hasNext()) + logger.error(null, iterator.next()); + return false; + + } + mSdkContext.setSdkLogger(factory); + mSdkContext.setSdkProgressIndicator(factory); + // TODO - Consider how to signal SDK loaded + //mSdkContext.getSdkHelper().broadcastOnSdkLoaded(mSdkContext.getSdkLog()); + + // Display packages + mPkgPage.onReady(factory); + return true; + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Called by the main loop when the window has been disposed. + */ + private void dispose() { + mLogWindow.close(); + } + + /** + * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. + */ + private boolean initializeSettings() { + Settings settings = new Settings(); + if (settings.initialize(mSdkContext.getSdkLog())) + { + mSdkContext.setSettings(settings); + return true; + } + return false; + } + + private void onToggleLogWindow() { + // toggle visibility + if (!mButtonShowLog.isDisposed()) { + mLogWindow.setVisible(!mLogWindow.isVisible()); + mButtonShowLog.setState(mLogWindow.isVisible() ? 1 : 0); + } + } + + private void onStopSelected() { + mSdkContext.getProgressIndicator().cancel(); + } + + private void onAvdManager() { + try { + AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( + mShell, + mSdkContext, + AvdInvocationContext.DIALOG); + + win.open(); + } catch (Exception e) { + mSdkContext.getSdkLog().error(e, "AVD Manager window error"); + } + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java new file mode 100644 index 00000000..bdb11e5f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java @@ -0,0 +1,166 @@ +/* + * 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.sdkuilib.internal.repository.ui; + + +import com.android.prefs.AndroidLocation; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Monitor; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +/** + * Utility to save & restore the size and position on a window + * using a common config file. + */ +public class ShellSizeAndPos { + + private static final String SETTINGS_FILENAME = "androidwin.cfg"; //$NON-NLS-1$ + private static final String PX = "_px"; //$NON-NLS-1$ + private static final String PY = "_py"; //$NON-NLS-1$ + private static final String SX = "_sx"; //$NON-NLS-1$ + private static final String SY = "_sy"; //$NON-NLS-1$ + + public static void loadSizeAndPos(Shell shell, String prefix) { + Properties props = loadProperties(); + + try { + int px = Integer.parseInt(props.getProperty(prefix + PX)); + int py = Integer.parseInt(props.getProperty(prefix + PY)); + int sx = Integer.parseInt(props.getProperty(prefix + SX)); + int sy = Integer.parseInt(props.getProperty(prefix + SY)); + + Point p1 = new Point(px, py); + Point p2 = new Point(px + sx, py + sy); + Rectangle r = new Rectangle(px, py, sy, sy); + + Monitor bestMatch = null; + int bestSurface = -1; + for (Monitor monitor : shell.getDisplay().getMonitors()) { + Rectangle area = monitor.getClientArea(); + if (area.contains(p1) && area.contains(p2)) { + // The shell is fully visible on this monitor. Just use that. + bestMatch = monitor; + bestSurface = Integer.MAX_VALUE; + break; + } else { + // Find which monitor displays the largest surface of the window. + // We'll use this one to center the window there, to make sure we're not + // starting split between several monitors. + Rectangle i = area.intersection(r); + int surface = i.width * i.height; + if (surface > bestSurface) { + bestSurface = surface; + bestMatch = monitor; + } + } + } + + if (bestMatch != null && bestSurface != Integer.MAX_VALUE) { + // Recenter the window on this monitor and make sure it fits + Rectangle area = bestMatch.getClientArea(); + + sx = Math.min(sx, area.width); + sy = Math.min(sy, area.height); + px = area.x + (area.width - sx) / 2; + py = area.y + (area.height - sy) / 2; + } + + shell.setLocation(px, py); + shell.setSize(sx, sy); + + } catch ( Exception e) { + // Ignore exception. We could typically get NPE from the getProperty + // or NumberFormatException from parseInt calls. Either way, do + // nothing if anything goes wrong. + } + } + + public static void saveSizeAndPos(Shell shell, String prefix) { + Properties props = loadProperties(); + + Point loc = shell.getLocation(); + Point size = shell.getSize(); + + props.setProperty(prefix + PX, Integer.toString(loc.x)); + props.setProperty(prefix + PY, Integer.toString(loc.y)); + props.setProperty(prefix + SX, Integer.toString(size.x)); + props.setProperty(prefix + SY, Integer.toString(size.y)); + + saveProperties(props); + } + + /** + * Load properties saved in {@link #SETTINGS_FILENAME}. + * If the file does not exists or doesn't load properly, just return an + * empty set of properties. + */ + private static Properties loadProperties() { + Properties props = new Properties(); + FileInputStream fis = null; + + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + if (f.exists()) { + fis = new FileInputStream(f); + + props.load(fis); + } + } catch (Exception e) { + // Ignore + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + + return props; + } + + private static void saveProperties(Properties props) { + FileOutputStream fos = null; + + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + fos = new FileOutputStream(f); + + props.store(fos, "## Size and Pos for SDK Manager Windows"); //$NON-NLS-1$ + + } catch (Exception e) { + // ignore + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java new file mode 100644 index 00000000..fbc73092 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java @@ -0,0 +1,50 @@ +/* + * 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.sdkuilib.internal.tasks; + + +/** + * Interface for a user interface that displays the log from a task monitor. + */ +public interface ILogUiProvider { + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + public abstract void setDescription(String description); + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + public abstract void log(String log); + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + public abstract void logError(String log); + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + public abstract void logVerbose(String log); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java new file mode 100644 index 00000000..23d4a416 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java @@ -0,0 +1,98 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; + +import org.eclipse.swt.widgets.ProgressBar; + +/** + * Interface for a user interface that displays both a task status + * (e.g. via an {@link ITaskMonitor}) and the progress state of the + * task (e.g. via a progress bar.) + *

+ * See {@link ITaskMonitor} for details on how a monitor expects to + * be displayed. + */ +interface IProgressUiProvider extends ILogUiProvider { + + public abstract boolean isCancelRequested(); + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public abstract void setDescription(String description); + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + public abstract void setProgressMax(int max); + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + public abstract void setProgress(int value); + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + public abstract int getProgress(); + + /** + * Display a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + public abstract boolean displayPrompt(String title, String message); + + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + public abstract void displayInfo(String title, String message); + + /** + * Launch an interface which asks for login credentials. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns user provided credentials + */ + public UserCredentials displayLoginCredentialsPrompt(String title, String message); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java new file mode 100644 index 00000000..ab5a91ca --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java @@ -0,0 +1,128 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.swt.widgets.Shell; + + +/** + * An {@link ITaskMonitor} that displays a {@link ProgressTaskDialog}. + */ +public final class ProgressTask extends TaskMonitorImpl { + + private final String mTitle; + private final ProgressTaskDialog mDialog; + private volatile boolean mAutoClose = true; + + + /** + * Creates a new {@link ProgressTask} with the given title. + * This does NOT start the task. The caller must invoke {@link #start(ITask)}. + */ + public ProgressTask(Shell parent, String title) { + super(new ProgressTaskDialog(parent)); + mTitle = title; + mDialog = (ProgressTaskDialog) getUiProvider(); + mDialog.setText(mTitle); + } + + /** + * Execute the given task asynchronously in a separate thread (not the UI thread). + * The {@link ProgressTask} must not be reused after this call. + * Returns when dialog is closed + * @param task The task to be executed + */ + public void start(ITask task) { + assert mDialog != null; + Job job = createJob(mTitle, task); + job.setPriority(Job.INTERACTIVE); + mDialog.open(job); + } + + /** + * Changes the auto-close behavior of the dialog on task completion. + * + * @param autoClose True if the dialog should be closed automatically when the task + * has completed. + */ + public void setAutoClose(boolean autoClose) { + if (autoClose != mAutoClose) { + if (autoClose) { + mDialog.setAutoCloseRequested(); + } else { + mDialog.setManualCloseRequested(); + } + mAutoClose = autoClose; + } + } + + /** + * Creates a Job to run the task. The caller must call schedule() on the job to start it. + * When the task completes, requests to close the dialog. + * @return A new job that will run the task. The thread has not been started yet. + */ + private Job createJob(String title, final ITask task) { + if (task != null) { + return new Job(title) { + @Override + protected IStatus run(IProgressMonitor m) { + IStatus status = Status.CANCEL_STATUS; + try { + task.run(ProgressTask.this); + if (mAutoClose) { + mDialog.setAutoCloseRequested(); + } else { + mDialog.setManualCloseRequested(); + } + status = Status.OK_STATUS; + } catch (Exception e) { + StringWriter builder = new StringWriter(); + builder.append(e.getMessage()).append("\n"); + PrintWriter writer = new PrintWriter(builder); + e.printStackTrace(writer); + logError(builder.toString()); + } finally { + } + return status; + } + }; + } + return null; + } + + /** + * {@inheritDoc} + *

+ * Sets the dialog to not auto-close since we want the user to see the error + * (this is equivalent to calling {@code setAutoClose(false)}). + */ + @Override + public void logError(String format, Object...args) { + setAutoClose(false); + super.logError(format, args); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java new file mode 100644 index 00000000..8f0fdd0a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java @@ -0,0 +1,556 @@ +/* + * 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.sdkuilib.internal.tasks; + +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +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.Label; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Widget; + +import com.android.SdkConstants; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; +import com.android.utils.Pair; + + +/** + * Implements a {@link ProgressTaskDialog}, used by the {@link ProgressTask} class. + * This separates the dialog UI from the task logic. + * + * Note: this does not implement the {@link ITaskMonitor} interface to avoid confusing + * SWT Designer. + */ +final class ProgressTaskDialog extends Dialog implements IProgressUiProvider { + + /** + * Min Y location for dialog. Need to deal with the menu bar on mac os. + */ + private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? + 20 : 0; + + private static enum CancelMode { + /** Cancel button says "Cancel" and is enabled. Waiting for user to cancel. */ + ACTIVE, + /** Cancel button has been clicked. Waiting for thread to finish. */ + CANCEL_PENDING, + /** Close pending. Close button clicked or thread finished but there were some + * messages so the user needs to manually close. */ + CLOSE_MANUAL, + /** Close button clicked or thread finished. The window will automatically close. */ + CLOSE_AUTO + } + + /** The current mode of operation of the dialog. */ + private CancelMode mCancelMode = CancelMode.ACTIVE; + + /** Last dialog size for this session. */ + private static Point sLastSize; + + + // UI fields + private Shell mDialogShell; + private Composite mRootComposite; + private Label mLabel; + private ProgressBar mProgressBar; + private Button mCancelButton; + private Text mResultText; + + + /** + * Create the dialog. + * @param parent Parent container + */ + public ProgressTaskDialog(Shell parent) { + super(parent, SWT.APPLICATION_MODAL); + } + + /** + * Open the dialog and blocks till it gets closed + */ + public void open(Job job) { + createContents(); + positionShell(); //$hide$ (hide from SWT designer) + mDialogShell.open(); + mDialogShell.layout(); + job.setPriority(Job.INTERACTIVE); + /* + if (onTerminateTask != null) { + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + onTerminateTask.run(); + } + }); + } + */ + job.schedule(); + + Display display = getParent().getDisplay(); + while (!mDialogShell.isDisposed() && mCancelMode != CancelMode.CLOSE_AUTO) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + setCancelRequested(); //$hide$ (hide from SWT designer) + if (!mDialogShell.isDisposed()) { + sLastSize = mDialogShell.getSize(); + mDialogShell.close(); + } + } + + + public void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + // Check again whether the widget got disposed between the time where + // we requested the syncExec and the time it actually happened. + if (!widget.isDisposed()) { + runnable.run(); + } + } + }); + } + } + /** + * Create contents of the dialog. + */ + private void createContents() { + mDialogShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE); + mDialogShell.addShellListener(new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + onShellClosed(e); + } + }); + mDialogShell.setLayout(new GridLayout(1, false)); + mDialogShell.setSize(450, 300); + mDialogShell.setText(getText()); + + mRootComposite = new Composite(mDialogShell, SWT.NONE); + mRootComposite.setLayout(new GridLayout(2, false)); + mRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mLabel = new Label(mRootComposite, SWT.NONE); + mLabel.setText("Task"); + mLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + mProgressBar = new ProgressBar(mRootComposite, SWT.NONE); + mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mCancelButton = new Button(mRootComposite, SWT.NONE); + mCancelButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + mCancelButton.setText("Cancel"); + + mCancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onCancelSelected(); //$hide$ + } + }); + + mResultText = new Text(mRootComposite, + SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | + SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); + mResultText.setEditable(true); + mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + } + + // -- End of UI, Start of internal logic ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + @Override + public boolean isCancelRequested() { + return mCancelMode != CancelMode.ACTIVE; + } + + /** + * Sets the mode to cancel pending. + * The first time this grays the cancel button, to let the user know that the + * cancel operation is pending. + */ + public void setCancelRequested() { + if (!mDialogShell.isDisposed()) { + // The dialog is not disposed, make sure to run all this in the UI thread + // and lock on the cancel button mode. + mDialogShell.getDisplay().syncExec(new Runnable() { + + @Override + public void run() { + synchronized (mCancelMode) { + if (mCancelMode == CancelMode.ACTIVE) { + mCancelMode = CancelMode.CANCEL_PENDING; + + if (!mCancelButton.isDisposed()) { + mCancelButton.setEnabled(false); + } + } + } + } + }); + } else { + // The dialog is disposed. Just set the boolean. We shouldn't be here. + if (mCancelMode == CancelMode.ACTIVE) { + mCancelMode = CancelMode.CANCEL_PENDING; + } + } + } + + /** + * Sets the mode to close manual. + * The first time, this also ungrays the pause button and converts it to a close button. + */ + public void setManualCloseRequested() { + if (!mDialogShell.isDisposed()) { + // The dialog is not disposed, make sure to run all this in the UI thread + // and lock on the cancel button mode. + mDialogShell.getDisplay().syncExec(new Runnable() { + + @Override + public void run() { + synchronized (mCancelMode) { + if (mCancelMode != CancelMode.CLOSE_MANUAL && + mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_MANUAL; + + if (!mCancelButton.isDisposed()) { + mCancelButton.setEnabled(true); + mCancelButton.setText("Close"); + } + } + } + } + }); + } else { + // The dialog is disposed. Just set the booleans. We shouldn't be here. + if (mCancelMode != CancelMode.CLOSE_MANUAL && + mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_MANUAL; + } + } + } + + /** + * Sets the mode to close auto. + * The main loop will just exit and close the shell at the first opportunity. + */ + public void setAutoCloseRequested() { + synchronized (mCancelMode) { + if (mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_AUTO; + } + } + } + + /** + * Callback invoked when the cancel button is selected. + * When in closing mode, this simply closes the shell. Otherwise triggers a cancel. + */ + private void onCancelSelected() { + if (mCancelMode == CancelMode.CLOSE_MANUAL) { + setAutoCloseRequested(); + } else { + setCancelRequested(); + } + } + + /** + * Callback invoked when the shell is closed either by clicking the close button + * on by calling shell.close(). + * This does the same thing as clicking the cancel/close button unless the mode is + * to auto close in which case we should do nothing to let the shell close normally. + */ + private void onShellClosed(ShellEvent e) { + if (mCancelMode != CancelMode.CLOSE_AUTO) { + e.doit = false; // don't close directly + onCancelSelected(); + } + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mLabel.isDisposed()) { + mLabel.setText(description); + } + } + }); + } + + /** + * Adds to the log in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(final String info) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mResultText.isDisposed()) { + mResultText.setVisible(true); + String lastText = mResultText.getText(); + if (lastText != null && + lastText.length() > 0 && + !lastText.endsWith("\n") && //$NON-NLS-1$ + !info.startsWith("\n")) { //$NON-NLS-1$ + mResultText.append("\n"); //$NON-NLS-1$ + } + mResultText.append(info); + } + } + }); + } + } + + @Override + public void logError(String info) { + log(info); + } + + @Override + public void logVerbose(String info) { + log(info); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + @Override + public void setProgressMax(final int max) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + mProgressBar.setMaximum(max); + } + } + }); + } + } + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setProgress(final int value) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + mProgressBar.setSelection(value); + } + } + }); + } + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + final int[] result = new int[] { 0 }; + + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + result[0] = mProgressBar.getSelection(); + } + } + }); + } + + return result[0]; + } + + /** + * Display a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + @Override + public boolean displayPrompt(final String title, final String message) { + Display display = mDialogShell.getDisplay(); + + // we need to ask the user what he wants to do. + final boolean[] result = new boolean[] { false }; + display.syncExec(new Runnable() { + @Override + public void run() { + result[0] = MessageDialog.openQuestion(mDialogShell, title, message); + } + }); + return result[0]; + } + + + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(String title, String message) { + Display display = mDialogShell.getDisplay(); + + display.syncExec(new Runnable() { + @Override + public void run() { + MessageDialog.openInformation(mDialogShell, title, message); + } + }); + } + + /** + * This method opens a pop-up window which requests for User Login and + * password. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) + */ + @Override + public UserCredentials displayLoginCredentialsPrompt( + final String title, final String message) { + Display display = mDialogShell.getDisplay(); + + // open dialog and request login and password + GetUserCredentialsTask task = new GetUserCredentialsTask(mDialogShell, title, message); + display.syncExec(task); + + return task.getUserCredentials(); + } + + private static class GetUserCredentialsTask implements Runnable { + private UserCredentials mResult = null; + + private Shell mShell; + private String mTitle; + private String mMessage; + + public GetUserCredentialsTask(Shell shell, String title, String message) { + mShell = shell; + mTitle = title; + mMessage = message; + } + + @Override + public void run() { + AuthenticationDialog authenticationDialog = new AuthenticationDialog(mShell, + mTitle, mMessage); + int dlgResult= authenticationDialog.open(); + if(dlgResult == GridDialog.OK) { + mResult = new UserCredentials( + authenticationDialog.getLogin(), + authenticationDialog.getPassword(), + authenticationDialog.getWorkstation(), + authenticationDialog.getDomain()); + } + } + + public UserCredentials getUserCredentials() { + return mResult; + } + } + + /** + * Centers the dialog in its parent shell. + */ + private void positionShell() { + // Centers the dialog in its parent shell + Shell child = mDialogShell; + Shell parent = getParent(); + if (child != null && parent != null) { + + // get the parent client area with a location relative to the display + Rectangle parentArea = parent.getClientArea(); + Point parentLoc = parent.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + // Reuse the last size if there's one, otherwise use the default + Point childSize = sLastSize != null ? sLastSize : child.getSize(); + int cw = childSize.x; + int ch = childSize.y; + + int x = px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < MIN_Y) y = MIN_Y; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java new file mode 100644 index 00000000..6c99bbcc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java @@ -0,0 +1,41 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; + +import org.eclipse.swt.widgets.Shell; + +/** + * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog + * for each new task. + */ +public final class ProgressTaskFactory implements ITaskFactory { + + private final Shell mShell; + + public ProgressTaskFactory(Shell shell) { + mShell = shell; + } + + @Override + public void start(String title, ITask task, Runnable onTerminateTask) { + ProgressTask p = new ProgressTask(mShell, title); + p.start(task); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java new file mode 100644 index 00000000..3fa604b8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java @@ -0,0 +1,424 @@ +/* + * 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.sdkuilib.internal.tasks; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; + + +/** + * Implements a "view" that uses an existing progress bar, status button and + * status text to display a {@link ITaskMonitor}. + */ +public final class ProgressView implements IProgressUiProvider { + + private static enum State { + /** View created but there's no task running. Next state can only be ACTIVE. */ + IDLE, + /** A task is currently running. Next state is either STOP_PENDING or IDLE. */ + ACTIVE, + /** Stop button has been clicked. Waiting for thread to finish. Next state is IDLE. */ + STOP_PENDING, + } + + /** The current mode of operation of the dialog. */ + private State mState = State.IDLE; + + + + // UI fields + private final Label mLabel; + private final Control mStopButton; + private final ProgressBar mProgressBar; + + /** Logger object. Cannot not be null. */ + private final ILogUiProvider mLog; + + /** + * Creates a new {@link ProgressView} object, a simple "holder" for the various + * widgets used to display and update a progress + status bar. + * + * @param label The label to display titles of status updates (e.g. task titles and + * calls to {@link #setDescription(String)}.) Must not be null. + * @param progressBar The progress bar to update during a task. Must not be null. + * @param stopButton The stop button. It will be disabled when there's no task that can + * be interrupted. A selection listener will be attached to it. Optional. Can be null. + * @param log A mandatory logger object that will be used to report all the log. + * Must not be null. + */ + public ProgressView( + Label label, + ProgressBar progressBar, + Control stopButton, + ILogUiProvider log) { + mLabel = label; + mProgressBar = progressBar; + mLog = log; + mProgressBar.setEnabled(false); + + mStopButton = stopButton; + if (mStopButton != null) { + mStopButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + if (mState == State.ACTIVE) { + changeState(State.STOP_PENDING); + } + } + }); + } + } + + /** + * Starts the task returns. Runs given callback when the task is either finished or canceled. + */ + public void startAsyncTask( + final String title, + final ITask task, + final Runnable onTerminateTask) { + // If for some reason the UI has been disposed, just abort the thread. + if (mProgressBar.isDisposed()) + return; + if (task != null) { + try { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mLabel.setText(title); + mProgressBar.setSelection(0); + mProgressBar.setEnabled(true); + } + }); + changeState(ProgressView.State.ACTIVE); + Job job = new Job(title) { + @Override + protected IStatus run(IProgressMonitor m) { + IStatus status = Status.CANCEL_STATUS; + try { + task.run(new TaskMonitorImpl(ProgressView.this)); + status = Status.OK_STATUS; + } catch (Exception e) { + StringWriter builder = new StringWriter(); + builder.append(e.getMessage()).append("\n"); + PrintWriter writer = new PrintWriter(builder); + e.printStackTrace(writer); + mLog.logError(builder.toString()); + } finally { + endTask(); + } + return status; + } + }; + job.setPriority(Job.INTERACTIVE); + if (onTerminateTask != null) { + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + onTerminateTask.run(); + } + }); + } + job.schedule(); + } catch (Exception e) { + StringWriter builder = new StringWriter(); + builder.append(e.getMessage()).append("\n"); + PrintWriter writer = new PrintWriter(builder); + e.printStackTrace(writer); + mLog.logError(builder.toString()); + } + } + } + + /** + * Starts the task and blocks until the task is completed or canceled. + */ + public void startSyncTask( + final String title, + final ITask task) { + // If for some reason the UI has been disposed, just abort the thread. + if (mProgressBar.isDisposed()) + return; + if (task != null) { + try { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mLabel.setText(title); + mProgressBar.setSelection(0); + mProgressBar.setEnabled(true); + } + }); + changeState(ProgressView.State.ACTIVE); + task.run(new TaskMonitorImpl(ProgressView.this)); + } catch (Exception e) { + StringWriter builder = new StringWriter(); + builder.append(e.getMessage()).append("\n"); + PrintWriter writer = new PrintWriter(builder); + e.printStackTrace(writer); + mLog.logError(builder.toString()); + } + finally { + endTask(); + } + } + } + + public void endTask() + { + changeState(ProgressView.State.IDLE); + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setSelection(0); + mProgressBar.setEnabled(false); + } + }); + } + + public void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + // Check again whether the widget got disposed between the time where + // we requested the syncExec and the time it actually happened. + if (!widget.isDisposed()) { + runnable.run(); + } + } + }); + } + } + + private void changeState(State state) { + if (mState != null ) { + mState = state; + } + + syncExec(mStopButton, new Runnable() { + @Override + public void run() { + if (mState == State.ACTIVE) + mStopButton.setEnabled(true); + } + }); + + } + + // --- Implementation of ITaskUiProvider --- + + @Override + public boolean isCancelRequested() { + boolean cancelStatus = mState != State.ACTIVE; + if (cancelStatus) + log("Stop button pressed"); + return cancelStatus; + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + syncExec(mLabel, new Runnable() { + @Override + public void run() { + mLabel.setText(description); + } + }); + + mLog.setDescription(description); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(String log) { + mLog.log(log); + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(String log) { + mLog.logError(log); + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(String log) { + mLog.logVerbose(log); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + @Override + public void setProgressMax(final int max) { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setMaximum(max); + } + }); + } + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setProgress(final int value) { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setSelection(value); + } + }); + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + final int[] result = new int[] { 0 }; + + if (!mProgressBar.isDisposed()) { + mProgressBar.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + result[0] = mProgressBar.getSelection(); + } + } + }); + } + + return result[0]; + } + + @Override + public boolean displayPrompt(final String title, final String message) { + final boolean[] result = new boolean[] { false }; + + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); + + return result[0]; + } + + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(String title, String message) { + + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + MessageDialog.openInformation(shell, title, message); + } + }); + } + + /** + * This method opens a pop-up window which requests for User Credentials. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns user provided credentials. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) + */ + @Override + public UserCredentials + displayLoginCredentialsPrompt(final String title, final String message) { + final AtomicReference result = new AtomicReference(null); + + // open dialog and request login and password + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + AuthenticationDialog authenticationDialog = new AuthenticationDialog(shell, + title, + message); + int dlgResult = authenticationDialog.open(); + if (dlgResult == GridDialog.OK) { + result.set(new UserCredentials( + authenticationDialog.getLogin(), + authenticationDialog.getPassword(), + authenticationDialog.getWorkstation(), + authenticationDialog.getDomain())); + } + } + }); + + return result.get(); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java new file mode 100644 index 00000000..6d27d15a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.tasks; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.android.repository.api.ProgressIndicator; +import com.android.sdkuilib.internal.repository.ITaskMonitor; + +/** + * @author Andrew Bowley + * + * 12-11-2017 + */ +public class SdkProgressIndicator implements ProgressIndicator { + // Use Monitor max count value convert fractions to integer values + private static final int MAX_COUNT = 10000; + + private final ITaskMonitor monitor; + private volatile boolean isCancelled = false; + private volatile boolean cancellable = true; + // TODO - implement indeterminate cursor + private volatile boolean indeterminate = false; + + /** + * + */ + public SdkProgressIndicator(ITaskMonitor monitor) { + this.monitor = monitor; + monitor.setProgressMax(MAX_COUNT); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#cancel() + */ + @Override + public void cancel() { + if (cancellable) + // TODO - Consider informing user request denied or disable stop button when not cancellable + isCancelled = true; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#getFraction() + */ + @Override + public double getFraction() { + return MAX_COUNT / monitor.getProgress(); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isCanceled() + */ + @Override + public boolean isCanceled() { + return isCancelled || monitor.isCancelRequested(); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isCancellable() + */ + @Override + public boolean isCancellable() { + return cancellable; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isIndeterminate() + */ + @Override + public boolean isIndeterminate() { + return indeterminate; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logError(java.lang.String) + */ + @Override + public void logError(String message) { + monitor.logError(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logError(java.lang.String, java.lang.Throwable) + */ + @Override + public void logError(String message, Throwable throwable) { + monitor.error(throwable, message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logInfo(java.lang.String) + */ + @Override + public void logInfo(String message) { + monitor.info(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logWarning(java.lang.String) + */ + @Override + public void logWarning(String message) { + monitor.warning(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logWarning(java.lang.String, java.lang.Throwable) + */ + @Override + public void logWarning(String message, Throwable throwable) { + StringWriter builder = new StringWriter(); + builder.append(message); + if (throwable != null) { + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + monitor.warning(builder.toString()); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setCancellable(boolean) + */ + @Override + public void setCancellable(boolean cancellable) { + this.cancellable = cancellable; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setFraction(double) + */ + @Override + public void setFraction(double fraction) { + int progress = fraction == 1.0 ? MAX_COUNT : (int)((double)MAX_COUNT * fraction); + monitor.incProgress(progress - monitor.getProgress()); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setIndeterminate(boolean) + */ + @Override + public void setIndeterminate(boolean indeterminate) { + this.indeterminate = indeterminate; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setSecondaryText(java.lang.String) + */ + @Override + public void setSecondaryText(String text) { + // TODO - implement secondary text + monitor.logVerbose(text); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setText(java.lang.String) + */ + @Override + public void setText(String text) { + monitor.setDescription(text); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java new file mode 100644 index 00000000..6eb55981 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java @@ -0,0 +1,390 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.annotations.NonNull; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; + +/** + * Implements the logic of an {@link ITaskMonitor}. + * It doesn't deal with any UI directly. Instead it delegates the UI to + * the provided {@link IProgressUiProvider}. + */ +class TaskMonitorImpl implements ITaskMonitor { + + private static final double MAX_COUNT = 10000.0; + + private interface ISubTaskMonitor extends ITaskMonitor { + public void subIncProgress(double realDelta); + } + + private double mIncCoef = 0; + private double mValue = 0; + private final IProgressUiProvider mUi; + + /** + * Returns true if the given {@code monitor} is an instance of {@link TaskMonitorImpl} + * or its private SubTaskMonitor. + */ + public static boolean isTaskMonitorImpl(ITaskMonitor monitor) { + return monitor instanceof TaskMonitorImpl || monitor instanceof SubTaskMonitor; + } + + /** + * Constructs a new {@link TaskMonitorImpl} that relies on the given + * {@link IProgressUiProvider} to change the user interface. + * @param ui The {@link IProgressUiProvider}. Cannot be null. + */ + public TaskMonitorImpl(IProgressUiProvider ui) { + mUi = ui; + } + + /** Returns the {@link IProgressUiProvider} passed to the constructor. */ + public IProgressUiProvider getUiProvider() { + return mUi; + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(String format, Object... args) { + final String text = String.format(format, args); + mUi.setDescription(text); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(String format, Object... args) { + String text = String.format(format, args); + mUi.log(text); + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(String format, Object... args) { + String text = String.format(format, args); + mUi.logError(text); + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(String format, Object... args) { + String text = String.format(format, args); + mUi.logVerbose(text); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * Weird things will happen if setProgressMax is called multiple times + * *after* {@link #incProgress(int)}: we don't try to adjust it on the + * fly. + */ + @Override + public void setProgressMax(int max) { + assert max > 0; + // Always set the dialog's progress max to 10k since it only handles + // integers and we want to have a better inner granularity. Instead + // we use the max to compute a coefficient for inc deltas. + mUi.setProgressMax((int) MAX_COUNT); + mIncCoef = max > 0 ? MAX_COUNT / max : 0; + assert mIncCoef > 0; + } + + @Override + public int getProgressMax() { + return mIncCoef > 0 ? (int) (MAX_COUNT / mIncCoef) : 0; + } + + /** + * Increments the current value of the progress bar. + * + * This method can be invoked from a non-UI thread. + */ + @Override + public void incProgress(int delta) { + if (delta > 0 && mIncCoef > 0) { + internalIncProgress(delta * mIncCoef); + } + } + + private void internalIncProgress(double realDelta) { + mValue += realDelta; + mUi.setProgress((int)mValue); + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + // mIncCoef is 0 if setProgressMax hasn't been used yet. + return mIncCoef > 0 ? (int)(mUi.getProgress() / mIncCoef) : 0; + } + + /** + * Returns true if the "Cancel" button was selected. + * It is up to the task thread to pool this and exit. + */ + @Override + public boolean isCancelRequested() { + return mUi.isCancelRequested(); + } + + /** + * Displays a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + @Override + public boolean displayPrompt(final String title, final String message) { + return mUi.displayPrompt(title, message); + } + + /** + * Display info dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(final String title, final String message) + { + mUi.displayInfo(title, message); + }; + + /** + * Displays a Login/Password dialog. This implementation allows this method to be + * called from any thread, it makes sure the dialog is opened synchronously + * in the ui thread. + * + * @param title The title of the dialog box + * @param message Message to be displayed + * @return Pair with entered login/password. Login is always the first + * element and Password is always the second. If any error occurs a + * pair with empty strings is returned. + */ + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return mUi.displayLoginCredentialsPrompt(title, message); + } + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mIncCoef > 0; + assert tickCount > 0; + return new SubTaskMonitor(this, null, mValue, tickCount * mIncCoef); + } + + // ----- ILogger interface ---- + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + if (errorFormat != null) { + logError("Error: " + errorFormat, arg); + } + + if (throwable != null) { + logError("%s", throwable.getMessage()); //$NON-NLS-1$ + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + log("Warning: " + warningFormat, arg); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + log(msgFormat, arg); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + log(msgFormat, arg); + } + + // ----- Sub Monitor ----- + + private static class SubTaskMonitor implements ISubTaskMonitor { + + private final TaskMonitorImpl mRoot; + private final ISubTaskMonitor mParent; + private final double mStart; + private final double mSpan; + private double mSubValue; + private double mSubCoef; + + /** + * Creates a new sub task monitor which will work for the given range [start, start+span] + * in its parent. + * + * @param taskMonitor The ProgressTask root + * @param parent The immediate parent. Can be the null or another sub task monitor. + * @param start The start value in the root's coordinates + * @param span The span value in the root's coordinates + */ + public SubTaskMonitor(TaskMonitorImpl taskMonitor, + ISubTaskMonitor parent, + double start, + double span) { + mRoot = taskMonitor; + mParent = parent; + mStart = start; + mSpan = span; + mSubValue = start; + } + + @Override + public boolean isCancelRequested() { + return mRoot.isCancelRequested(); + } + + @Override + public void setDescription(String format, Object... args) { + mRoot.setDescription(format, args); + } + + @Override + public void log(String format, Object... args) { + mRoot.log(format, args); + } + + @Override + public void logError(String format, Object... args) { + mRoot.logError(format, args); + } + + @Override + public void logVerbose(String format, Object... args) { + mRoot.logVerbose(format, args); + } + + @Override + public void setProgressMax(int max) { + assert max > 0; + mSubCoef = max > 0 ? mSpan / max : 0; + assert mSubCoef > 0; + } + + @Override + public int getProgressMax() { + return mSubCoef > 0 ? (int) (mSpan / mSubCoef) : 0; + } + + @Override + public int getProgress() { + // subCoef can be 0 if setProgressMax() and incProgress() haven't been called yet + assert mSubValue == mStart || mSubCoef > 0; + return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0; + } + + @Override + public void incProgress(int delta) { + if (delta > 0 && mSubCoef > 0) { + subIncProgress(delta * mSubCoef); + } + } + + @Override + public void subIncProgress(double realDelta) { + mSubValue += realDelta; + if (mParent != null) { + mParent.subIncProgress(realDelta); + } else { + mRoot.internalIncProgress(realDelta); + } + } + + @Override + public boolean displayPrompt(String title, String message) { + return mRoot.displayPrompt(title, message); + } + + @Override + public void displayInfo(String title, String message) { + mRoot.displayInfo(title, message); + } + + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return mRoot.displayLoginCredentialsPrompt(title, message); + } + + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mSubCoef > 0; + assert tickCount > 0; + return new SubTaskMonitor(mRoot, + this, + mSubValue, + tickCount * mSubCoef); + } + + // ----- ILogger interface ---- + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + mRoot.error(throwable, errorFormat, arg); + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + mRoot.warning(warningFormat, arg); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + mRoot.info(msgFormat, arg); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + mRoot.verbose(msgFormat, arg); + } + + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java new file mode 100644 index 00000000..4c633cc2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java @@ -0,0 +1,65 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.annotations.NonNull; +import com.android.sdklib.devices.Device; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.widgets.Shell; + +/** + * Displays a dialog to create an AVD. + *

+ * Implementation: the code is split using a simplified MVP pattern.
+ * {@link AvdCreationPresenter} contains all the logic of the dialog and performs all the + * actions and event handling.
+ * {@link AvdCreationSwtView} creates the SWT shell and maps the controls and forwards all + * events to the presenter. The presenter uses the {@link IWidgetAdapter} exposed by the + * SwtView class to update the display. + *

+ * To transform this dialog to Swing or a text-based unit-test, simply keep the presenter + * as-is and re-implement the {@link IWidgetAdapter}. + */ +public class AvdCreationDialog extends AvdCreationSwtView { + + public AvdCreationDialog(Shell shell, + @NonNull SdkContext sdkContext, + @NonNull SdkTargets sdkTargets, + AvdAgent editAvdAgent) { + super(shell, sdkContext.getSdkHelper().getImageFactory(), new AvdCreationPresenter(sdkContext, sdkTargets, editAvdAgent)); + + } + + /** Returns the AVD Created, if successful. */ + public AvdInfo getCreatedAvd() { + return getPresenter().getCreatedAvd(); + } + + /** + * Can be called after the constructor to set the default device for this AVD. + * Useful especially for new AVDs. + * @param device + */ + public void selectInitialDevice(@NonNull Device device) { + getPresenter().selectInitialDevice(device); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java new file mode 100644 index 00000000..a42eb431 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java @@ -0,0 +1,1473 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkuilib.internal.widgets; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Pattern; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.resources.Density; +import com.android.resources.ScreenSize; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.devices.Camera; +import com.android.sdklib.devices.CameraLocation; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.Software; +import com.android.sdklib.devices.Storage; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.avd.HardwareProperties; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.widgets.MessageBoxLog; +import com.android.utils.ILogger; +import com.android.utils.Pair; +import com.google.common.base.Joiner; + +/** + * Implements all the logic of the {@link AvdCreationDialog}. + *

+ * Implementation detail: the dialog is split using a simple MVP pattern.
+ * This code, the presenter, handles all the logic including event handling and controls + * what is displayed in the dialog. It does not directly manipulate the UI and in fact the + * code has no swt import. Instead the constructor is given a {@link IWidgetAdapter} + * implementation. This makes it possible to test the logic of the dialog as-is + * in a unit test by simply passing a different {@link IWidgetAdapter} implementation to the + * constructor. + */ +class AvdCreationPresenter { + public enum AvdConflict + { + NO_CONFLICT, CONFLICT_EXISTING_AVD, CONFLICT_INVALID_AVD, CONFLICT_EXISTING_PATH; + + private AvdConflict() {} + } + public static final Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); + public static final String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; + + @NonNull + private IWidgetAdapter mWidgets; + private AvdManager mAvdManager; + private ILogger mSdkLog; + private AvdInfo mAvdInfo; + private AvdAgent mAvdAgent; + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; + + private final TreeMap mCurrentTargets = + new TreeMap(); + + private static final AvdSkinChoice SKIN_DYNAMIC = + new AvdSkinChoice(SkinType.DYNAMIC, "Skin with dynamic hardware controls"); + private static final AvdSkinChoice SKIN_NONE = + new AvdSkinChoice(SkinType.NONE, "No skin"); + + private final List mComboDevices = new ArrayList<>(); + private final List mComboSkins = new ArrayList<>(); + private final List mComboSystemImages = new ArrayList(); + private final List mComboTargets = new ArrayList(); + + private Device mInitWithDevice; + private AvdInfo mCreatedAvd; + + public enum Ctrl { + BUTTON_OK, + BUTTON_BROWSE_SDCARD, + + COMBO_DEVICE, + COMBO_TARGET, + COMBO_TAG_ABI, + COMBO_SKIN, + COMBO_FRONT_CAM, + COMBO_BACK_CAM, + COMBO_DATA_PART_SIZE, + COMBO_SDCARD_SIZE, + + CHECK_FORCE_CREATION, + CHECK_KEYBOARD, + CHECK_SNAPSHOT, + CHECK_GPU_EMUL, + RADIO_SDCARD_SIZE, + RADIO_SDCARD_FILE, + + TEXT_AVD_NAME, + TEXT_RAM, + TEXT_VM_HEAP, + TEXT_DATA_PART, + TEXT_SDCARD_SIZE, + TEXT_SDCARD_FILE, + + ICON_STATUS, + TEXT_STATUS, + } + + /** Interface exposed by the view. Presenter calls this to update UI. */ + public interface IWidgetAdapter { + void setTitle(@NonNull String title); + int getComboIndex(@NonNull Ctrl ctrl); + int getComboSize (@NonNull Ctrl ctrl); + void selectComboIndex(@NonNull Ctrl ctrl, int index); + String getComboItem (@NonNull Ctrl ctrl, int index); + void addComboItem (@NonNull Ctrl ctrl, String label); + /** Set combo labels or clear combo if labels is null. */ + void setComboItems(@NonNull Ctrl ctrl, @Nullable String[] labels); + boolean isEnabled(@NonNull Ctrl ctrl); + void setEnabled(@NonNull Ctrl ctrl, boolean enabled); + boolean isChecked(@NonNull Ctrl ctrl); + void setChecked(@NonNull Ctrl ctrl, boolean checked); + String getText(@NonNull Ctrl ctrl); + void setText(@NonNull Ctrl ctrl, String text); + void setImage(@NonNull Ctrl ctrl, @Nullable String imageName); + String openFileDialog(@NonNull String string); + void repack(); + + /** + * Creates a MessageBoxLog, a special MessageBox that returns a logger instance + * to capture logs. The caller then performs actions and outputs to the logger + * and at the end calls MessageBoxLog.displayResult() to create a message box + * with the result of the logged output. + */ + IMessageBoxLogger newDelayedMessageBoxLog(String title, boolean logErrorsOnly); + + } + + public AvdCreationPresenter(@NonNull SdkContext sdkContext, + @NonNull SdkTargets sdkTargets, + @Nullable AvdAgent editAvdAgent) { + mSdkContext = sdkContext; + mSdkTargets = sdkTargets; + mAvdManager = sdkContext.getAvdManager(); + mSdkLog = mSdkContext.getSdkLog(); + mAvdAgent = editAvdAgent; + if (editAvdAgent != null) + mAvdInfo = editAvdAgent.getAvd(); + } + + /** Returns the AVD Created, if successful. */ + public AvdInfo getCreatedAvd() { + return mCreatedAvd; + } + + /** Called by the view constructor to set the view updater. */ + public void setWidgetAdapter(@NonNull IWidgetAdapter widgetAdapter) { + mWidgets = widgetAdapter; + } + + /** + * Can be called after the constructor to set the default device for this AVD. + * Useful especially for new AVDs. + */ + public void selectInitialDevice(@NonNull Device device) { + mInitWithDevice = device; + } + + public void onViewInit() { + mWidgets.setTitle( + mAvdInfo == null ? "Create new Android Virtual Device (AVD)" + : "Edit Android Virtual Device (AVD)"); + initializeDevices(); + + // setup the 2 default choices (no skin, dynamic skin); do not select any right now. + mComboSkins.add(SKIN_DYNAMIC); + mComboSkins.add(SKIN_NONE); + Collections.sort(mComboSkins); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, mComboSkins.get(0).getLabel()); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, mComboSkins.get(1).getLabel()); + + // Preload target combo *after* ABI/Tag and Skin combos have been setup as + // they will be setup depending on the selected target. + preloadTargetCombo(); + + toggleCameras(); + + enableSdCardWidgets(true); + + + if (mAvdInfo != null) { + fillExistingAvdInfo(mAvdAgent); + } else if (mInitWithDevice != null) { + fillInitialDeviceInfo(mInitWithDevice); + } + + validatePage(); + } + + //------- + + + + private void initializeDevices() { + DeviceManager deviceManager = mSdkContext.getDeviceManager(); + List deviceList = new ArrayList<>(deviceManager.getDevices(DeviceManager.ALL_DEVICES)); + + // Sort + Collections.sort(deviceList, Device.getDisplayComparator()); + + mComboDevices.clear(); + mComboDevices.addAll(deviceList); + + String[] labels = new String[mComboDevices.size()]; + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + Device device = mComboDevices.get(i); + labels[i] = getGenericLabel(device); + } + + mWidgets.setComboItems(Ctrl.COMBO_DEVICE, labels); + } + + @Nullable + private Device getSelectedDevice() { + int index = mWidgets.getComboIndex(Ctrl.COMBO_DEVICE); + if (index != -1 && index < mComboDevices.size()) { + return mComboDevices.get(index); + } + + return null; + } + + /** Called by fillExisting/InitialDeviceInfo to select the device in the combo list. */ + @SuppressWarnings("deprecation") + private void selectDevice(String manufacturer, String name) { + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + Device device = mComboDevices.get(i); + if (device.getManufacturer().equals(manufacturer) + && device.getName().equals(name)) { + mWidgets.selectComboIndex(Ctrl.COMBO_DEVICE, i); + break; + } + } + } + + /** Called by fillExisting/InitialDeviceInfo to select the device in the combo list. */ + private void selectDevice(Device device) { + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + if (mComboDevices.get(i).equals(device)) { + mWidgets.selectComboIndex(Ctrl.COMBO_DEVICE, i); + break; + } + } + } + + void onDeviceComboChanged() { + Device currentDevice = getSelectedDevice(); + if (currentDevice != null) { + fillDeviceProperties(currentDevice); + } + + toggleCameras(); + validatePage(); + } + + void onAvdNameModified() { + String name = mWidgets.getText(Ctrl.TEXT_AVD_NAME).trim(); + if (mAvdInfo == null || !name.equals(mAvdInfo.getName())) { + // Case where we're creating a new AVD or editing an existing one + // and the AVD name has been changed... check for name uniqueness. + + Pair conflict = isAvdNameConflicting(name); + if (conflict.getFirst() != AvdConflict.NO_CONFLICT) { + // If we're changing the state from disabled to enabled, make sure + // to uncheck the button, to force the user to voluntarily re-enforce it. + // This happens when editing an existing AVD and changing the name from + // the existing AVD to another different existing AVD. + if (!mWidgets.isEnabled(Ctrl.CHECK_FORCE_CREATION)) { + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, true); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + } else { + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, false); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + } else { + // Case where we're editing an existing AVD with the name unchanged. + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, false); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + validatePage(); + + } + + + + private void fillDeviceProperties(Device device) { + Hardware hw = device.getDefaultHardware(); + Long ram = hw.getRam().getSizeAsUnit(Storage.Unit.MiB); + mWidgets.setText(Ctrl.TEXT_RAM, Long.toString(ram)); + + // Set the default VM heap size. This is based on the Android CDD minimums for each + // screen size and density. + Screen s = hw.getScreen(); + ScreenSize size = s.getSize(); + Density density = s.getPixelDensity(); + int vmHeapSize = 32; + if (size.equals(ScreenSize.XLARGE)) { + switch (density) { + case LOW: + case MEDIUM: + vmHeapSize = 32; + break; + case TV: + case HIGH: + vmHeapSize = 64; + break; + case XHIGH: + case XXHIGH: + case XXXHIGH: + vmHeapSize = 128; + break; + case NODPI: + break; + case ANYDPI: + break; + case DPI_260: + break; + case DPI_280: + break; + case DPI_300: + break; + case DPI_340: + break; + case DPI_360: + break; + case DPI_400: + break; + case DPI_420: + break; + case DPI_560: + break; + default: + break; + } + } else { + switch (density) { + case LOW: + case MEDIUM: + vmHeapSize = 16; + break; + case TV: + case HIGH: + vmHeapSize = 32; + break; + case XHIGH: + case XXHIGH: + case XXXHIGH: + vmHeapSize = 64; + break; + case NODPI: + break; + case ANYDPI: + break; + case DPI_260: + break; + case DPI_280: + break; + case DPI_300: + break; + case DPI_340: + break; + case DPI_360: + break; + case DPI_400: + break; + case DPI_420: + break; + case DPI_560: + break; + default: + break; + } + } + mWidgets.setText(Ctrl.TEXT_VM_HEAP, Integer.toString(vmHeapSize)); + + boolean reloadTabAbiCombo = false; + + List allSoftware = device.getAllSoftware(); + if (allSoftware != null && !allSoftware.isEmpty()) { + Software first = allSoftware.get(0); + int min = first.getMinSdkLevel();; + int max = first.getMaxSdkLevel();; + for (int i = 1; i < allSoftware.size(); i++) { + min = Math.min(min, first.getMinSdkLevel()); + max = Math.max(max, first.getMaxSdkLevel()); + } + if (mCurrentTargets != null) { + int bestApiLevel = Integer.MAX_VALUE; + IAndroidTarget bestTarget = null; + for (IAndroidTarget target : mCurrentTargets.values()) { + if (!target.isPlatform()) { + continue; + } + int apiLevel = target.getVersion().getApiLevel(); + if (apiLevel >= min && apiLevel <= max) { + if (bestTarget == null || apiLevel < bestApiLevel) { + bestTarget = target; + bestApiLevel = apiLevel; + } + } + } + + if (bestTarget != null) { + selectTarget(bestTarget); + reloadTabAbiCombo = true; + } + } + } + + if (!reloadTabAbiCombo) { + String deviceTagId = device.getTagId(); + Pair currTagAbi = getSelectedTagAbi(); + if (deviceTagId != null && + (currTagAbi == null || !deviceTagId.equals(currTagAbi.getFirst().getId()))) { + reloadTabAbiCombo = true; + } + } + + if (reloadTabAbiCombo) { + reloadTagAbiCombo(); + } + } + + private void toggleCameras() { + mWidgets.setEnabled(Ctrl.COMBO_FRONT_CAM, false); + mWidgets.setEnabled(Ctrl.COMBO_BACK_CAM, false); + Device d = getSelectedDevice(); + if (d != null) { + for (Camera c : d.getDefaultHardware().getCameras()) { + if (CameraLocation.FRONT.equals(c.getLocation())) { + mWidgets.setEnabled(Ctrl.COMBO_FRONT_CAM, true); + } + if (CameraLocation.BACK.equals(c.getLocation())) { + mWidgets.setEnabled(Ctrl.COMBO_BACK_CAM, true); + } + } + } + } + + private void preloadTargetCombo() { + String selected = null; + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index >= 0) { + selected = mWidgets.getComboItem(Ctrl.COMBO_TARGET, index); + } + + mCurrentTargets.clear(); + mWidgets.setComboItems(Ctrl.COMBO_TARGET, null); + + boolean found = false; + index = -1; + + mComboTargets.clear(); + for (IAndroidTarget target : mSdkTargets.getSystemImageTargets()) { + String name; + if (target.isPlatform()) { + name = target.getFullName(); + } else { + name = target.getName(); + if (!name.equals(target.getVendor())) { + name = String.format("%s (%s) - API Level %s", + name, + target.getVendor(), + target.getVersion().getApiString()); + } else { + name = String.format("%s - API Level %s", + name, + target.getVersion().getApiString()); + } + } + mCurrentTargets.put(name, target); + mWidgets.addComboItem(Ctrl.COMBO_TARGET, name); + mComboTargets.add(target); + if (!found) { + index++; + found = name.equals(selected); + } + } + mWidgets.setEnabled(Ctrl.COMBO_TARGET, mCurrentTargets.size() > 0); + + if (found) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, index); + } + + reloadTagAbiCombo(); + } + + private void selectTarget(IAndroidTarget target) { + for (int i = 0, n = mComboTargets.size(); i < n; i++) { + if (target == mComboTargets.get(i)) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, i); + break; + } + } + } + + private IAndroidTarget getSelectedTarget() { + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index != -1 && index < mComboTargets.size()) { + return mComboTargets.get(index); + } + + return null; + } + + /** + * Reload all the abi types in the selection list. + * Also adds/remove the skin choices embedded in a tag/abi, if any. + */ + void reloadTagAbiCombo() { + + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index >= 0) { + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, index); + IAndroidTarget target = mCurrentTargets.get(targetName); + + Collection systemImages = getSystemImages(target); + // If user explicitly selected an ABI before, preserve that option + // If user did not explicitly select before (only one option before) + // force them to select + String selected = null; + index = mWidgets.getComboIndex(Ctrl.COMBO_TAG_ABI); + if (index >= 0 && mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI) > 1) { + selected = mWidgets.getComboItem(Ctrl.COMBO_TAG_ABI, index); + } + + // if there's a selected device that requires a specific non-default tag-id, + // filter the list to only match this tag. + Device currDevice = getSelectedDevice(); + String deviceTagId = currDevice == null ? null : currDevice.getTagId(); + if (deviceTagId != null && + (deviceTagId.isEmpty() || SystemImage.DEFAULT_TAG.equals(deviceTagId))) { + deviceTagId = null; + } + + // filter and create the list + mWidgets.setComboItems(Ctrl.COMBO_TAG_ABI, null); + mComboSystemImages.clear(); + + int i = 0; + boolean found = false; + Iterator iterator = systemImages.iterator(); + while(iterator.hasNext()) { + SystemImage systemImage = iterator.next(); + if (deviceTagId != null && !deviceTagId.equals(systemImage.getTag().getId())) { + continue; + } + String prettyAbiType = AvdInfo.getPrettyAbiType(systemImage); + mComboSystemImages.add(systemImage); + mWidgets.addComboItem(Ctrl.COMBO_TAG_ABI, prettyAbiType); + if (!found) { + found = prettyAbiType.equals(selected); + if (found) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); + } + } + ++i; + } + + mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, !mComboSystemImages.isEmpty()); + + if (mComboSystemImages.isEmpty()) { + mWidgets.addComboItem(Ctrl.COMBO_TAG_ABI, "No system images installed for this target."); + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, 0); + } else if (mComboSystemImages.size() == 1) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, 0); + } + } + reloadSkinCombo(); + } + + void reloadSkinCombo() { + AvdSkinChoice selected = getSelectedSkinChoice(); + + // Remove existing target & tag skins + for (Iterator it = mComboSkins.iterator(); it.hasNext(); ) { + AvdSkinChoice choice = it.next(); + if (choice.hasPath()) { + it.remove(); + } + } + + IAndroidTarget target = getSelectedTarget(); + if (target != null) { + ISystemImage sysImg = getSelectedSysImg(); + Set sysImgSkins = new HashSet(); + if (sysImg != null) { + sysImgSkins.addAll(Arrays.asList(sysImg.getSkins())); + } + + // path of sdk/system-images + String sdkSysImgPath = new File(mSdkContext.getLocation(), + SdkConstants.FD_SYSTEM_IMAGES).getAbsolutePath(); + + for (File skin : target.getSkins()) { + String label = skin.getName(); + String skinPath = skin.getAbsolutePath(); + if (skinPath.startsWith(sdkSysImgPath)) { + if (sysImg == null) { + // Reject a sys-img based skin if no sys img is selected + continue; + } + if (!sysImgSkins.contains(skin)) { + // If a skin comes from a tagged system-image, only display + // those matching the current system image. + continue; + } + if (!SystemImage.DEFAULT_TAG.equals(sysImg.getTag().getId())) { + // Append the tag name if it's not the similar to the label. + String display = sysImg.getTag().getDisplay(); + String azDisplay = display.toLowerCase(Locale.US).replaceAll("[^a-z]", ""); + String azLabel = label .toLowerCase(Locale.US).replaceAll("[^a-z]", ""); + if (!azLabel.contains(azDisplay)) { + label = String.format("%s (%s)", label, display); + } + } + } + AvdSkinChoice sc = new AvdSkinChoice(SkinType.FROM_TARGET, label, skin); + mComboSkins.add(sc); + } + } + + Collections.sort(mComboSkins); + + mWidgets.setComboItems(Ctrl.COMBO_SKIN, null); + for (int i = 0; i < mComboSkins.size(); i++) { + AvdSkinChoice choice = mComboSkins.get(i); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, choice.getLabel()); + if (choice == selected) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + } + } + + } + + /** + * Enable or disable the sd card widgets. + * + * @param sizeMode if true the size-based widgets are to be enabled, and the + * file-based ones disabled. + */ + void enableSdCardWidgets(boolean sizeMode) { + mWidgets.setEnabled(Ctrl.TEXT_SDCARD_SIZE, sizeMode); + mWidgets.setEnabled(Ctrl.COMBO_SDCARD_SIZE, sizeMode); + + mWidgets.setEnabled(Ctrl.TEXT_SDCARD_FILE, !sizeMode); + mWidgets.setEnabled(Ctrl.BUTTON_BROWSE_SDCARD, !sizeMode); + } + + void onBrowseSdCard() { + String fileName = mWidgets. openFileDialog("Choose SD Card image file."); + if (fileName != null) { + mWidgets.setText(Ctrl.TEXT_SDCARD_FILE, fileName); + } + validatePage(); + } + + void validatePage() { + String error = null; + ArrayList warnings = new ArrayList(); + boolean valid = true; + + String avdName = mWidgets.getText(Ctrl.TEXT_AVD_NAME); + + if (avdName.isEmpty()) { + error = "AVD Name cannot be empty"; + setPageValid(false, error, null); + return; + } + + if (!RE_AVD_NAME.matcher(avdName).matches()) { + error = String.format( + "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", + avdName, CHARS_AVD_NAME); + setPageValid(false, error, null); + return; + } + + if (mWidgets.getComboIndex(Ctrl.COMBO_DEVICE) < 0) { + error = "No device selected"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getComboIndex(Ctrl.COMBO_TARGET) < 0) { + error = "No target selected"; + setPageValid(false, error, null); + return; + } + + if (mComboSystemImages.isEmpty()) { + error = "No CPU/ABI system image available for this target"; + setPageValid(false, error, null); + return; + } else if (getSelectedTagAbi() == null) { + error = "No CPU/ABI system image selected"; + setPageValid(false, error, null); + return; + } + + // If the target is an addon, check its base platform requirement is satisfied. + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, mWidgets.getComboIndex(Ctrl.COMBO_TARGET)); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target != null && !target.isPlatform()) { + + Collection sis = mSdkTargets.getSystemImages(target); + if (sis != null && sis.size() > 0) { + // Note: if an addon has no system-images of its own, it depends on its parent + // platform and it wouldn't have been loaded properly if the platform were + // missing so we don't need to double-check that part here. + + Pair tagAbi = getSelectedTagAbi(); + IdDisplay tag = tagAbi.getFirst(); + String abiType = tagAbi.getSecond(); + if (abiType != null && + !abiType.isEmpty() && + getSystemImage(target.getParent(), tag, abiType) == null) { + // We have a system-image requirement but there is no such system image + // loaded in the parent platform. This AVD won't run properly. + warnings.add( + String.format( + "This AVD may not work unless you install the %1$s system image " + + "for %2$s (%3$s) first.", + AvdInfo.getPrettyAbiType(tag, abiType), + target.getParent().getName(), + target.getParent().getVersion().toString())); + } + } + } + + AvdSkinChoice skinChoice = getSelectedSkinChoice(); + if (skinChoice == null) { + error = "No skin selected"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_RAM).isEmpty()) { + error = "Mising RAM value"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_VM_HEAP).isEmpty()) { + error = "Mising VM Heap value"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_DATA_PART).isEmpty() || mWidgets.getComboIndex(Ctrl.COMBO_DATA_PART_SIZE) < 0) { + error = "Invalid Data partition size."; + setPageValid(false, error, null); + return; + } + + // validate sdcard size or file + if (mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE)) { + if (!mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE).isEmpty() && mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE) >= 0) { + try { + long sdSize = Long.parseLong(mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE)); + + int sizeIndex = mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE); + if (sizeIndex >= 0) { + // index 0 shifts by 10 (1024=K), index 1 by 20, etc. + sdSize <<= 10 * (1 + sizeIndex); + } + + if (sdSize < AvdManager.SDCARD_MIN_BYTE_SIZE || + sdSize > AvdManager.SDCARD_MAX_BYTE_SIZE) { + valid = false; + error = "SD Card size is invalid. Range is 9 MiB..1023 GiB."; + } + } catch (NumberFormatException e) { + valid = false; + error = " SD Card size must be a valid integer between 9 MiB and 1023 GiB"; + } + } + } else { + if (mWidgets.getText(Ctrl.TEXT_SDCARD_FILE).isEmpty() || + !new File(mWidgets.getText(Ctrl.TEXT_SDCARD_FILE)).isFile()) { + valid = false; + error = "SD Card path isn't valid."; + } + } + if (!valid) { + setPageValid(valid, error, null); + return; + } + + if (mWidgets.isEnabled(Ctrl.CHECK_FORCE_CREATION) && !mWidgets.isChecked(Ctrl.CHECK_FORCE_CREATION)) { + valid = false; + error = String.format( + "The AVD name '%s' is already used.\n" + + "Check \"Override the existing AVD\" to delete the existing one.", + mWidgets.getText(Ctrl.TEXT_AVD_NAME)); + } + + if (mAvdInfo != null && !mAvdInfo.getName().equals(mWidgets.getText(Ctrl.TEXT_AVD_NAME))) { + warnings.add( + String.format("The AVD '%1$s' will be duplicated into '%2$s'.", + mAvdInfo.getName(), + mWidgets.getText(Ctrl.TEXT_AVD_NAME))); + } + + // On Windows, display a warning if attempting to create AVD's with RAM > 512 MB. + // This restriction should go away when we switch to using a 64 bit emulator. + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + long ramSize = 0; + try { + ramSize = Long.parseLong(mWidgets.getText(Ctrl.TEXT_RAM)); + } catch (NumberFormatException e) { + // ignore + } + + if (ramSize > 768) { + warnings.add( + "On Windows, emulating RAM greater than 768M may fail depending on the" + + " system load. Try progressively smaller values of RAM if the emulator" + + " fails to launch."); + } + } + + if (mWidgets.isChecked(Ctrl.CHECK_GPU_EMUL) && mWidgets.isChecked(Ctrl.CHECK_SNAPSHOT)) { + valid = false; + error = "GPU Emulation and Snapshot cannot be used simultaneously"; + } + + String warning = Joiner.on('\n').join(warnings); + setPageValid(valid, error, warning); + return; + } + + private void setPageValid(boolean valid, String error, String warning) { + mWidgets.setEnabled(Ctrl.BUTTON_OK, valid); + if (error != null && !error.isEmpty()) { + mWidgets.setImage(Ctrl.ICON_STATUS, "reject_icon16.png"); //$NON-NLS-1$ + mWidgets.setText(Ctrl.TEXT_STATUS, error); + } else if (warning != null && !warning.isEmpty()) { + mWidgets.setImage(Ctrl.ICON_STATUS, "warning_icon16.png"); //$NON-NLS-1$ + mWidgets.setText(Ctrl.TEXT_STATUS, warning); + } else { + mWidgets.setImage(Ctrl.ICON_STATUS, null); + mWidgets.setText(Ctrl.TEXT_STATUS, " \n "); //$NON-NLS-1$ + } + + mWidgets.repack(); + } + + boolean createAvd() { + String avdName = mWidgets.getText(Ctrl.TEXT_AVD_NAME); + if (avdName == null || avdName.isEmpty()) { + return false; + } + + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, mWidgets.getComboIndex(Ctrl.COMBO_TARGET)); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target == null) { + return false; + } + ISystemImage systemImage = getSelectedSysImg(); + if (systemImage == null) + return false; + // get the SD card data from the UI. + String sdName = null; + if (mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE)) { + // size mode + String value = mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE).trim(); + if (value.length() > 0) { + sdName = value; + // add the unit + switch (mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE)) { + case 0: + sdName += "K"; //$NON-NLS-1$ + break; + case 1: + sdName += "M"; //$NON-NLS-1$ + break; + case 2: + sdName += "G"; //$NON-NLS-1$ + break; + default: + // shouldn't be here + assert false; + } + } + } else { + // file mode. + sdName = mWidgets.getText(Ctrl.TEXT_SDCARD_FILE).trim(); + } + + // Get the device + Device device = getSelectedDevice(); + if (device == null) { + return false; + } + + File skinFolder = null; + String skinName = null; + AvdSkinChoice skinChoice = getSelectedSkinChoice(); + if (skinChoice == null) { + return false; + } + if (skinChoice.hasPath()) { + skinFolder = skinChoice.getPath(); + } else { + Screen s = device.getDefaultHardware().getScreen(); + skinName = s.getXDimension() + "x" + s.getYDimension(); + } + + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = mWidgets.newDelayedMessageBoxLog( + String.format("Result of creating AVD '%s':", avdName), + false /* logErrorsOnly */); + } + + Map hwProps = DeviceManager.getHardwareProperties(device); + if (mWidgets.isChecked(Ctrl.CHECK_GPU_EMUL)) { + hwProps.put(AvdManager.AVD_INI_GPU_EMULATION, HardwareProperties.BOOLEAN_YES); + } + + File avdFolder = null; + try { + avdFolder = AvdInfo.getDefaultAvdFolder( + mAvdManager, avdName, mSdkContext.getHandler().getFileOp(), false); + } catch (AndroidLocationException e) { + return false; + } + + // Although the device has this information, some devices have more RAM than we'd want to + // allocate to an emulator. + hwProps.put(AvdManager.AVD_INI_RAM_SIZE, mWidgets.getText(Ctrl.TEXT_RAM)); + hwProps.put(AvdManager.AVD_INI_VM_HEAP_SIZE, mWidgets.getText(Ctrl.TEXT_VM_HEAP)); + + String suffix; + switch (mWidgets.getComboIndex(Ctrl.COMBO_DATA_PART_SIZE)) { + case 0: + suffix = "M"; + break; + case 1: + suffix = "G"; + break; + default: + suffix = "K"; + } + hwProps.put(AvdManager.AVD_INI_DATA_PARTITION_SIZE, + mWidgets.getText(Ctrl.TEXT_DATA_PART) + suffix); + + hwProps.put(HardwareProperties.HW_KEYBOARD, + mWidgets.isChecked(Ctrl.CHECK_KEYBOARD) ? + HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO); + + hwProps.put(AvdManager.AVD_INI_SKIN_DYNAMIC, + skinChoice.getType() == SkinType.DYNAMIC ? + HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO); + + if (mWidgets.isEnabled(Ctrl.COMBO_FRONT_CAM)) { + hwProps.put(AvdManager.AVD_INI_CAMERA_FRONT, + mWidgets.getText(Ctrl.COMBO_FRONT_CAM).toLowerCase()); + } + + if (mWidgets.isEnabled(Ctrl.COMBO_BACK_CAM)) { + hwProps.put(AvdManager.AVD_INI_CAMERA_BACK, + mWidgets.getText(Ctrl.COMBO_BACK_CAM).toLowerCase()); + } + + if (sdName != null) { + hwProps.put(HardwareProperties.HW_SDCARD, HardwareProperties.BOOLEAN_YES); + } + + /* + @NonNull File avdFolder, + @NonNull String avdName, + @NonNull ISystemImage systemImage, + @Nullable File skinFolder, + @Nullable String skinName, + @Nullable String sdcard, + @Nullable Map hardwareConfig, + @Nullable Map bootProps, + boolean deviceHasPlayStore, + boolean createSnapshot, + boolean removePrevious, + boolean editExisting, + @NonNull ILogger log) { + */ + AvdInfo avdInfo = mAvdManager.createAvd(avdFolder, + avdName, + systemImage, + skinFolder, + skinName, + sdName, + hwProps, + device.getBootProps(), + false, // deviceHasPlayStore + mWidgets.isChecked(Ctrl.CHECK_SNAPSHOT), + mWidgets.isChecked(Ctrl.CHECK_FORCE_CREATION), + mAvdInfo != null, // edit existing + log); + + mCreatedAvd = avdInfo; + boolean success = avdInfo != null; + + if (log instanceof IMessageBoxLogger) { + ((IMessageBoxLogger) log).displayResult(success); + } + return success; + } + + @Nullable + private AvdSkinChoice getSelectedSkinChoice() { + int choiceIndex = mWidgets.getComboIndex(Ctrl.COMBO_SKIN); + if (choiceIndex >= 0 && choiceIndex < mComboSkins.size()) { + return mComboSkins.get(choiceIndex); + } + return null; + } + + @Nullable + private Pair getSelectedTagAbi() { + ISystemImage selected = getSelectedSysImg(); + if (selected != null) { + return Pair.of(selected.getTag(), selected.getAbiType()); + } + return null; + } + + @Nullable + private ISystemImage getSelectedSysImg() { + if (!mComboSystemImages.isEmpty()) { + int abiIndex = mWidgets.getComboIndex(Ctrl.COMBO_TAG_ABI); + if (abiIndex >= 0 && abiIndex < mComboSystemImages.size()) { + return mComboSystemImages.get(abiIndex); + } + } + return null; + } + + private void fillExistingAvdInfo(AvdAgent avdAgent) { + mWidgets.setText(Ctrl.TEXT_AVD_NAME, avdAgent.getAvd().getName()); + selectDevice(avdAgent.getDeviceMfctr(), avdAgent.getDeviceName()); + toggleCameras(); + + IAndroidTarget target = avdAgent.getTarget(); + + if (target != null && !mCurrentTargets.isEmpty()) { + // Try to select the target in the target combo. + // This will fail if the AVD needs to be repaired. + // + // This is a linear search but the list is always + // small enough and we only do this once. + int n = mWidgets.getComboSize(Ctrl.COMBO_TARGET); + for (int i = 0; i < n; i++) { + if (target.equals(mCurrentTargets.get(mWidgets.getComboItem(Ctrl.COMBO_TARGET, i)))) { + // Note: combo.select does not trigger the combo's widgetSelected callback. + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, i); + reloadTagAbiCombo(); + break; + } + } + } + Collection systemImages = getSystemImages(target); + if (target != null && systemImages.size() > 0) { + mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, systemImages.size() > 1); + String abiType = mAvdAgent.getPrettyAbiType(); + int count = mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI); + for (int i = 0; i < count; i++) { + if (abiType.equals(mWidgets.getComboItem(Ctrl.COMBO_TAG_ABI, i))) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); + reloadSkinCombo(); + break; + } + } + } + + Map props = avdAgent.getAvd().getProperties(); + + if (props != null) { + String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshots != null && snapshots.length() > 0) { + mWidgets.setChecked(Ctrl.CHECK_SNAPSHOT, snapshots.equals("true")); + } + + String gpuEmulation = props.get(AvdManager.AVD_INI_GPU_EMULATION); + mWidgets.setChecked(Ctrl.CHECK_GPU_EMUL, gpuEmulation != null && + gpuEmulation.equals(HardwareProperties.BOOLEAN_YES)); + + String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH); + if (sdcard != null && sdcard.length() > 0) { + enableSdCardWidgets(false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_SIZE, false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_FILE, true); + mWidgets.setText(Ctrl.TEXT_SDCARD_FILE, sdcard); + } + + String ramSize = props.get(AvdManager.AVD_INI_RAM_SIZE); + if (ramSize != null) { + mWidgets.setText(Ctrl.TEXT_RAM, ramSize); + } + + String vmHeapSize = props.get(AvdManager.AVD_INI_VM_HEAP_SIZE); + if (vmHeapSize != null) { + mWidgets.setText(Ctrl.TEXT_VM_HEAP, vmHeapSize); + } + + String dataPartitionSize = props.get(AvdManager.AVD_INI_DATA_PARTITION_SIZE); + if (dataPartitionSize != null) { + mWidgets.setText(Ctrl.TEXT_DATA_PART, + dataPartitionSize.substring(0, dataPartitionSize.length() - 1)); + switch (dataPartitionSize.charAt(dataPartitionSize.length() - 1)) { + case 'M': + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, 0); + break; + case 'G': + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, 1); + break; + default: + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, -1); + } + } + + mWidgets.setChecked(Ctrl.CHECK_KEYBOARD, + HardwareProperties.BOOLEAN_YES.equalsIgnoreCase( + props.get(HardwareProperties.HW_KEYBOARD))); + + SkinType defaultSkinType = SkinType.NONE; + // the AVD .ini skin path is relative to the SDK folder *or* is a numeric size. + String skinIniPath = props.get(AvdManager.AVD_INI_SKIN_PATH); + if (skinIniPath != null) { + File skinFolder = getAvdPath(skinIniPath); + if (skinFolder != null) + for (int i = 0; i < mComboSkins.size(); i++) { + if (mComboSkins.get(i).hasPath() && + skinFolder.equals(mComboSkins.get(i).getPath())) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + defaultSkinType = null; + break; + } + } + } + + if (defaultSkinType != null) { + if (HardwareProperties.BOOLEAN_YES.equalsIgnoreCase( + props.get(AvdManager.AVD_INI_SKIN_DYNAMIC))) { + defaultSkinType = SkinType.DYNAMIC; + } + + for (int i = 0; i < mComboSkins.size(); i++) { + if (mComboSkins.get(i).getType() == defaultSkinType) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + break; + } + } + + } + + String cameraFront = props.get(AvdManager.AVD_INI_CAMERA_FRONT); + if (cameraFront != null) { + for (int i = 0, n = mWidgets.getComboSize(Ctrl.COMBO_FRONT_CAM); i < n; i++) { + String item = mWidgets.getComboItem(Ctrl.COMBO_FRONT_CAM, i); + if (item.toLowerCase().equals(cameraFront)) { + mWidgets.selectComboIndex(Ctrl.COMBO_FRONT_CAM, i); + break; + } + } + } + + String cameraBack = props.get(AvdManager.AVD_INI_CAMERA_BACK); + if (cameraBack != null) { + for (int i = 0, n = mWidgets.getComboSize(Ctrl.COMBO_BACK_CAM); i < n; i++) { + String item = mWidgets.getComboItem(Ctrl.COMBO_BACK_CAM, i); + if (item.toLowerCase().equals(cameraBack)) { + mWidgets.selectComboIndex(Ctrl.COMBO_BACK_CAM, i); + break; + } + } + } + + sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard != null && sdcard.length() > 0) { + String[] values = new String[2]; + long sdcardSize = AvdManager.parseSdcardSize(sdcard, values); + + if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) { + enableSdCardWidgets(true); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_FILE, false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_SIZE, true); + + mWidgets.setText(Ctrl.TEXT_SDCARD_SIZE, values[0]); + + String suffix = values[1]; + int n = mWidgets.getComboSize(Ctrl.COMBO_SDCARD_SIZE); + for (int i = 0; i < n; i++) { + if (mWidgets.getComboItem(Ctrl.COMBO_SDCARD_SIZE, i).startsWith(suffix)) { + mWidgets.selectComboIndex(Ctrl.COMBO_SDCARD_SIZE, i); + } + } + } + } + } + } + + private File getAvdPath(String subDirectory) + { + File file = null; + try { + file = new File(mAvdManager.getBaseAvdFolder(), subDirectory); + } catch (AndroidLocationException e) { + mSdkContext.getSdkLog().error(e, subDirectory); + } + return file; + } + + + private void fillInitialDeviceInfo(Device device) { + String name = "AVD for " + device.getDisplayName(); + // sanitize the name + //name = name.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_"); + mWidgets.setText(Ctrl.TEXT_AVD_NAME, name); + + // Select the device + selectDevice(device); + toggleCameras(); + + // If there's only one target, select it by default. + // TODO: if there are more than 1 target, select the higher platform target as + // a likely default. + if (mWidgets.getComboSize(Ctrl.COMBO_TARGET) == 1) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, 0); + reloadTagAbiCombo(); + } + + fillDeviceProperties(device); + } + + /** + * Returns the list of system images of a target. + *

+ * If target is null, returns an empty list. If target is an add-on with no + * system images, return the list from its parent platform. + * + * @param target An IAndroidTarget. Can be null. + * @return A non-null ISystemImage array. Can be empty. + */ + @NonNull + private Collection getSystemImages(IAndroidTarget target) { + return mSdkTargets.getSystemImages(target); + } + + private static String getGenericLabel(Device d) { + return String.format(java.util.Locale.US, "%1$s (%2$s)", d.getDisplayName(), + getResolutionString(d)); + } + + @Nullable + private static String getResolutionString(Device device) { + Screen screen = device.getDefaultHardware().getScreen(); + return String.format(java.util.Locale.US, + "%1$d \u00D7 %2$d: %3$s", // U+00D7: Unicode multiplication sign + screen.getXDimension(), + screen.getYDimension(), + screen.getPixelDensity().getResourceValue()); + } + + /** + * AVD skin type. Order defines the order of the skin combo list. + */ + private enum SkinType { + DYNAMIC, + NONE, + FROM_TARGET, + } + + /* + * Choice of AVD skin: dynamic, no skin, or one from the target. + * The 2 "internals" skins (dynamic and no skin) have no path. + * The target-based skins have a path. + */ + private static class AvdSkinChoice implements Comparable { + + private final SkinType mType; + private final String mLabel; + private final File mPath; + + AvdSkinChoice(@NonNull SkinType type, @NonNull String label) { + this(type, label, null); + } + + AvdSkinChoice(@NonNull SkinType type, @NonNull String label, @NonNull File path) { + mType = type; + mLabel = label; + mPath = path; + } + + @NonNull + public SkinType getType() { + return mType; + } + + @NonNull + public String getLabel() { + return mLabel; + } + + @Nullable + public File getPath() { + return mPath; + } + + public boolean hasPath() { + return mType == SkinType.FROM_TARGET; + } + + @Override + public int compareTo(AvdSkinChoice o) { + int t = mType.compareTo(o.mType); + if (t == 0) { + t = mLabel.compareTo(o.mLabel); + } + if (t == 0 && mPath != null && o.mPath != null) { + t = mPath.compareTo(o.mPath); + } + return t; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mType == null) ? 0 : mType.hashCode()); + result = prime * result + ((mLabel == null) ? 0 : mLabel.hashCode()); + result = prime * result + ((mPath == null) ? 0 : mPath.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof AvdSkinChoice)) { + return false; + } + AvdSkinChoice other = (AvdSkinChoice) obj; + if (mType != other.mType) { + return false; + } + if (mLabel == null) { + if (other.mLabel != null) { + return false; + } + } else if (!mLabel.equals(other.mLabel)) { + return false; + } + if (mPath == null) { + if (other.mPath != null) { + return false; + } + } else if (!mPath.equals(other.mPath)) { + return false; + } + return true; + } + + + } + + public void onTargetComboChanged() { + reloadTagAbiCombo(); + validatePage(); + } + + public void onTagComboChanged() { + reloadSkinCombo(); + validatePage(); + } + + public void onRadioSdCardSizeChanged() { + boolean sizeMode = mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE); + enableSdCardWidgets(sizeMode); + validatePage(); + } + + private ISystemImage getSystemImage(IAndroidTarget target, IdDisplay tag, String abiType) + { + if (target != null) + { + Collection systemImages = getSystemImages(target); + for (SystemImage sysImg : systemImages) { + if ((sysImg.getTag().equals(tag)) && (sysImg.getAbiType().equals(abiType))) { + return sysImg; + } + } + } + return null; + } + + private Pair isAvdNameConflicting(String name) + { + boolean ignoreCase = SdkConstants.currentPlatform() == 2; + AvdInfo[] allAvdList = mAvdManager.getAllAvds(); + for (AvdInfo info : allAvdList) + { + String name2 = info.getName(); + if ((name2.equals(name)) || ((ignoreCase) && (name2.equalsIgnoreCase(name)))) + { + if (info.getStatus() == AvdInfo.AvdStatus.OK) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_AVD, name2); + } + return Pair.of(AvdConflict.CONFLICT_INVALID_AVD, name2); + } + } + try + { + File file = AvdInfo.getDefaultIniFile(mAvdManager, name); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + file = AvdInfo.getDefaultAvdFolder(mAvdManager, name, mSdkContext.getHandler().getFileOp(), false); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + } + catch (AndroidLocationException e) {} + return Pair.of(AvdConflict.NO_CONFLICT, null); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java new file mode 100644 index 00000000..7dc1ad8d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java @@ -0,0 +1,610 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkuilib.internal.widgets; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.Ctrl; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.widgets.MessageBoxLog; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.HashMap; +import java.util.Map; + +/** + * Creates the SWT shell and controls for the {@link AvdCreationDialog}. + * All the logic is handled by the {@link AvdCreationPresenter}. + * + * @see AvdCreationPresenter + */ +class AvdCreationSwtView extends GridDialog { + + private final ImageFactory mImageFactory; + private final AvdCreationPresenter mPresenter; + + private Button mBtnOK; + + private Text mTextAvdName; + + private Combo mComboDevice; + + private Combo mComboTarget; + private Combo mComboTagAbi; + + private Button mCheckKeyboard; + private Combo mComboSkinCombo; + + private Combo mComboFrontCamera; + private Combo mComboBackCamera; + + private Button mCheckSnapshot; + private Button mCheckGpuEmulation; + + private Text mTextRam; + private Text mTextVmHeap; + + private Text mTextDataPartition; + private Combo mComboDataPartitionSize; + + private Button mRadioSdCardSize; + private Text mTextSdCardSize; + private Combo mComboSdCardSize; + private Button mRadioSdCardFile; + private Text mTextSdCardFile; + private Button mBtnBrowseSdCard; + + private Button mCheckForceCreation; + private Composite mCompositeStatus; + + private Label mIconStatus; + private Label mTextStatus; + + private final Map mControlMap = new HashMap(); + + /** + * {@link VerifyListener} for {@link Text} widgets that should only contains + * numbers. + */ + private final VerifyListener mDigitVerifier = new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + int count = event.text.length(); + for (int i = 0; i < count; i++) { + char c = event.text.charAt(i); + if (c < '0' || c > '9') { + event.doit = false; + return; + } + } + } + }; + + public AvdCreationSwtView(Shell shell, + @NonNull ImageFactory imageFactory, + @NonNull AvdCreationPresenter presenter) { + super(shell, 2, false); + mImageFactory = imageFactory; + mPresenter = presenter; + setShellStyle(getShellStyle() | SWT.RESIZE); + + mPresenter.setWidgetAdapter(new IWidgetAdapter() { + @Override + public void setTitle(@NonNull String title) { + getShell().setText(title); + } + + @Override + public int getComboIndex(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getSelectionIndex(); + } + return -1; + } + + @Override + public int getComboSize(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getItemCount(); + } + return 0; + } + + @Nullable + @Override + public String getComboItem(@NonNull Ctrl ctrl, int index) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getItem(index); + } + return null; + } + + @Override + public void selectComboIndex(@NonNull Ctrl ctrl, int index) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + ((Combo) c).select(index); + } + } + + @Override + public void addComboItem(@NonNull Ctrl ctrl, String label) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + ((Combo) c).add(label); + } + } + + @Override + public void setComboItems(@NonNull Ctrl ctrl, String[] labels) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + Combo combo = ((Combo) c); + combo.removeAll(); + if (labels != null) { + combo.setItems(labels); + } + } + } + + @Override + public boolean isEnabled(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c != null) { + return c.isEnabled(); + } + return false; + } + + @Override + public void setEnabled(@NonNull Ctrl ctrl, boolean enabled) { + Control c = mControlMap.get(ctrl); + if (c != null) { + c.setEnabled(enabled); + } + } + + @Override + public boolean isChecked(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Button) { + return ((Button) c).getSelection(); + } + return false; + } + + @Override + public void setChecked(@NonNull Ctrl ctrl, boolean checked) { + Control c = mControlMap.get(ctrl); + if (c instanceof Button) { + ((Button) c).setSelection(checked); + } + } + + @Override + public String getText(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Text) { + return ((Text) c).getText(); + } else if (c instanceof Combo) { + return ((Combo) c).getText(); + } else if (c instanceof Label) { + return ((Label) c).getText(); + } else if (c instanceof Button) { + return ((Button) c).getText(); + } + return null; + } + + @Override + public void setText(@NonNull Ctrl ctrl, @NonNull String text) { + Control c = mControlMap.get(ctrl); + if (c instanceof Text) { + ((Text) c).setText(text); + } else if (c instanceof Combo) { + ((Combo) c).setText(text); + } else if (c instanceof Label) { + ((Label) c).setText(text); + } else if (c instanceof Button) { + ((Button) c).setText(text); + } + } + + @Override + public void setImage(@NonNull Ctrl ctrl, @Nullable String imageName) { + Control c = mControlMap.get(ctrl); + if (c instanceof Label) { + ((Label) c).setImage( + imageName == null ? null : mImageFactory.getImageByName(imageName)); + } + } + + @Nullable + @Override + public String openFileDialog(@NonNull String title) { + FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN); + dlg.setText(title); + return dlg.open(); + } + + @Override + public void repack() { + mCompositeStatus.pack(true); + getShell().layout(true, true); + } + + @Override + public IMessageBoxLogger newDelayedMessageBoxLog(String title, boolean logErrorsOnly) { + return new MessageBoxLog(title, getContents().getDisplay(), logErrorsOnly); + } + }); + } + + @NonNull + public AvdCreationPresenter getPresenter() { + return mPresenter; + } + + @Override + protected Control createContents(Composite parent) { + // super.createContents will call createDialogContent() + // below and then continue here. + Control control = super.createContents(parent); + getShell().setMinimumSize(new Point(350, 600)); + + mBtnOK = getButton(IDialogConstants.OK_ID); + + registerControlMap(); + mPresenter.onViewInit(); + + return control; + } + + @Override + public void createDialogContent(Composite parent) { + Label label; + String tooltip; + ValidateListener validateListener = new ValidateListener(); + + // --- avd name + label = new Label(parent, SWT.NONE); + label.setText("AVD Name:"); + tooltip = "The name of the Android Virtual Device"; + label.setToolTipText(tooltip); + mTextAvdName = new Text(parent, SWT.BORDER); + mTextAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextAvdName.addModifyListener(new CreateNameModifyListener()); + + // --- device selection + label = new Label(parent, SWT.NONE); + label.setText("Device:"); + tooltip = "The device this AVD will be based on"; + mComboDevice = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboDevice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboDevice.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onDeviceComboChanged(); + } + }); + + // --- api target + label = new Label(parent, SWT.NONE); + label.setText("Target:"); + tooltip = "The target API of the AVD"; + label.setToolTipText(tooltip); + mComboTarget = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboTarget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboTarget.setToolTipText(tooltip); + mComboTarget.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mPresenter.onTargetComboChanged(); + } + }); + + // --- avd ABIs + label = new Label(parent, SWT.NONE); + label.setText("CPU/ABI:"); + tooltip = "The CPU/ABI of the virtual device"; + label.setToolTipText(tooltip); + mComboTagAbi = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboTagAbi.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboTagAbi.setToolTipText(tooltip); + mComboTagAbi.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mPresenter.onTagComboChanged(); + } + }); + + label = new Label(parent, SWT.NONE); + label.setText("Keyboard:"); + mCheckKeyboard = new Button(parent, SWT.CHECK); + mCheckKeyboard.setSelection(true); // default to having a keyboard irrespective of device + mCheckKeyboard.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mCheckKeyboard.setText("Hardware keyboard present"); + + // --- skins + label = new Label(parent, SWT.NONE); + label.setText("Skin:"); + mComboSkinCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboSkinCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboSkinCombo.addSelectionListener(validateListener); + + // --- camera + label = new Label(parent, SWT.NONE); + label.setText("Front Camera:"); + tooltip = ""; + label.setToolTipText(tooltip); + mComboFrontCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboFrontCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboFrontCamera.add("None"); + mComboFrontCamera.add("Emulated"); + mComboFrontCamera.add("Webcam0"); + mComboFrontCamera.select(0); + + label = new Label(parent, SWT.NONE); + label.setText("Back Camera:"); + tooltip = ""; + label.setToolTipText(tooltip); + mComboBackCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboBackCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboBackCamera.add("None"); + mComboBackCamera.add("Emulated"); + mComboBackCamera.add("Webcam0"); + mComboBackCamera.select(0); + + // --- memory options group + label = new Label(parent, SWT.NONE); + label.setText("Memory Options:"); + + Group memoryGroup = new Group(parent, SWT.NONE); + memoryGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + memoryGroup.setLayout(new GridLayout(4, false)); + + label = new Label(memoryGroup, SWT.NONE); + label.setText("RAM:"); + tooltip = "The amount of RAM the emulated device should have in MiB"; + label.setToolTipText(tooltip); + mTextRam = new Text(memoryGroup, SWT.BORDER); + mTextRam.addVerifyListener(mDigitVerifier); + mTextRam.addModifyListener(validateListener); + mTextRam.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + label = new Label(memoryGroup, SWT.NONE); + label.setText("VM Heap:"); + tooltip = "The amount of memory, in MiB, available to typical Android applications"; + label.setToolTipText(tooltip); + mTextVmHeap = new Text(memoryGroup, SWT.BORDER); + mTextVmHeap.addVerifyListener(mDigitVerifier); + mTextVmHeap.addModifyListener(validateListener); + mTextVmHeap.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextVmHeap.setToolTipText(tooltip); + + // --- Data partition group + label = new Label(parent, SWT.NONE); + label.setText("Internal Storage:"); + tooltip = "The size of the data partition on the device."; + Group storageGroup = new Group(parent, SWT.NONE); + storageGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + storageGroup.setLayout(new GridLayout(2, false)); + mTextDataPartition = new Text(storageGroup, SWT.BORDER); + mTextDataPartition.setText("200"); + mTextDataPartition.addVerifyListener(mDigitVerifier); + mTextDataPartition.addModifyListener(validateListener); + mTextDataPartition.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboDataPartitionSize = new Combo(storageGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboDataPartitionSize.add("MiB"); + mComboDataPartitionSize.add("GiB"); + mComboDataPartitionSize.select(0); + mComboDataPartitionSize.addModifyListener(validateListener); + + // --- sd card group + label = new Label(parent, SWT.NONE); + label.setText("SD Card:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group sdCardGroup = new Group(parent, SWT.NONE); + sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sdCardGroup.setLayout(new GridLayout(3, false)); + + mRadioSdCardSize = new Button(sdCardGroup, SWT.RADIO); + mRadioSdCardSize.setText("Size:"); + mRadioSdCardSize.setToolTipText("Create a new SD Card file"); + mRadioSdCardSize.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onRadioSdCardSizeChanged(); + } + }); + + mTextSdCardSize = new Text(sdCardGroup, SWT.BORDER); + mTextSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextSdCardSize.addVerifyListener(mDigitVerifier); + mTextSdCardSize.addModifyListener(validateListener); + mTextSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)"); + + mComboSdCardSize = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY); + mComboSdCardSize.add("KiB"); + mComboSdCardSize.add("MiB"); + mComboSdCardSize.add("GiB"); + mComboSdCardSize.select(1); + mComboSdCardSize.addSelectionListener(validateListener); + + mRadioSdCardFile = new Button(sdCardGroup, SWT.RADIO); + mRadioSdCardFile.setText("File:"); + mRadioSdCardFile.setToolTipText("Use an existing file for the SD Card"); + + mTextSdCardFile = new Text(sdCardGroup, SWT.BORDER); + mTextSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextSdCardFile.addModifyListener(validateListener); + mTextSdCardFile.setToolTipText("File to use for the SD Card"); + + mBtnBrowseSdCard = new Button(sdCardGroup, SWT.PUSH); + mBtnBrowseSdCard.setText("Browse..."); + mBtnBrowseSdCard.setToolTipText("Select the file to use for the SD Card"); + mBtnBrowseSdCard.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onBrowseSdCard(); + } + }); + + mRadioSdCardSize.setSelection(true); + + // --- avd options group + label = new Label(parent, SWT.NONE); + label.setText("Emulation Options:"); + Group optionsGroup = new Group(parent, SWT.NONE); + optionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + optionsGroup.setLayout(new GridLayout(2, true)); + mCheckSnapshot = new Button(optionsGroup, SWT.CHECK); + mCheckSnapshot.setText("Snapshot"); + mCheckSnapshot.setToolTipText("Emulator's state will be persisted between emulator executions"); + mCheckSnapshot.addSelectionListener(validateListener); + mCheckGpuEmulation = new Button(optionsGroup, SWT.CHECK); + mCheckGpuEmulation.setText("Use Host GPU"); + mCheckGpuEmulation.setToolTipText("Enable hardware OpenGLES emulation"); + mCheckGpuEmulation.addSelectionListener(validateListener); + + // --- force creation group + mCheckForceCreation = new Button(parent, SWT.CHECK); + mCheckForceCreation.setText("Override the existing AVD with the same name"); + mCheckForceCreation + .setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); + mCheckForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, + true, false, 2, 1)); + mCheckForceCreation.setEnabled(false); + mCheckForceCreation.addSelectionListener(validateListener); + + // add a separator to separate from the ok/cancel button + label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + + // add stuff for the error display + mCompositeStatus = new Composite(parent, SWT.NONE); + mCompositeStatus.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, + true, false, 3, 1)); + GridLayout gl; + mCompositeStatus.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mIconStatus = new Label(mCompositeStatus, SWT.NONE); + mIconStatus.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + mTextStatus = new Label(mCompositeStatus, SWT.WRAP); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); + // allow for approx 3 lines of text corresponding to the number of lines in the longest + // error or warning + gridData.heightHint = 50; + mTextStatus.setLayoutData(gridData); + mTextStatus.setText(""); //$NON-NLS-1$ + } + + private void registerControlMap() { + mControlMap.put(Ctrl.BUTTON_OK, mBtnOK); + mControlMap.put(Ctrl.BUTTON_BROWSE_SDCARD, mBtnBrowseSdCard); + + mControlMap.put(Ctrl.COMBO_DEVICE, mComboDevice); + mControlMap.put(Ctrl.COMBO_TARGET, mComboTarget); + mControlMap.put(Ctrl.COMBO_TAG_ABI, mComboTagAbi); + mControlMap.put(Ctrl.COMBO_SKIN, mComboSkinCombo); + mControlMap.put(Ctrl.COMBO_FRONT_CAM, mComboFrontCamera); + mControlMap.put(Ctrl.COMBO_BACK_CAM, mComboBackCamera); + mControlMap.put(Ctrl.COMBO_DATA_PART_SIZE, mComboDataPartitionSize); + mControlMap.put(Ctrl.COMBO_SDCARD_SIZE, mComboSdCardSize); + + mControlMap.put(Ctrl.CHECK_FORCE_CREATION, mCheckForceCreation); + mControlMap.put(Ctrl.CHECK_KEYBOARD, mCheckKeyboard); + mControlMap.put(Ctrl.CHECK_SNAPSHOT, mCheckSnapshot); + mControlMap.put(Ctrl.CHECK_GPU_EMUL, mCheckGpuEmulation); + mControlMap.put(Ctrl.RADIO_SDCARD_SIZE, mRadioSdCardSize); + mControlMap.put(Ctrl.RADIO_SDCARD_FILE, mRadioSdCardFile); + + mControlMap.put(Ctrl.TEXT_AVD_NAME, mTextAvdName); + mControlMap.put(Ctrl.TEXT_RAM, mTextRam); + mControlMap.put(Ctrl.TEXT_VM_HEAP, mTextVmHeap); + mControlMap.put(Ctrl.TEXT_DATA_PART, mTextDataPartition); + mControlMap.put(Ctrl.TEXT_SDCARD_SIZE, mTextSdCardSize); + mControlMap.put(Ctrl.TEXT_SDCARD_FILE, mTextSdCardFile); + + mControlMap.put(Ctrl.ICON_STATUS, mIconStatus); + mControlMap.put(Ctrl.TEXT_STATUS, mTextStatus); + } + + /** + * {@link ModifyListener} used for live-validation of the fields content. + */ + private class ValidateListener extends SelectionAdapter implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + mPresenter.validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + mPresenter.validatePage(); + } + } + + /** + * Callback when the AVD name is changed. When creating a new AVD, enables + * the force checkbox if the name is a duplicate. When editing an existing + * AVD, it's OK for the name to match the existing AVD. + */ + private class CreateNameModifyListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + mPresenter.onAvdNameModified(); + } + } + + @Override + public void okPressed() { + if (mPresenter.createAvd()) { + super.okPressed(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java new file mode 100644 index 00000000..b4172a43 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java @@ -0,0 +1,164 @@ +/* + * 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.sdkuilib.internal.widgets; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; + +/** + * Dialog displaying the details of an AVD. + */ +final class AvdDetailsDialog extends SwtBaseDialog { + + private final AvdAgent mAvdAgent; + private volatile int row = 0; + + public AvdDetailsDialog(Shell shell, AvdAgent avdAgent) { + super(shell, SWT.APPLICATION_MODAL, "AVD details"); + mAvdAgent = avdAgent; + } + + /** + * Create contents of the dialog. + */ + @Override + protected void createContents() { + Shell shell = getShell(); + GridLayoutBuilder.create(shell).columns(2); + GridDataBuilder.create(shell).fill(); + + GridLayout gl; + + Composite c = new Composite(shell, SWT.NONE); + c.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + GridData gridData = new GridData(); + gridData.horizontalAlignment = GridData.FILL; + gridData.grabExcessHorizontalSpace = true; + c.setLayoutData(gridData); + //Display display = c.getDisplay(); + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + //c.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + + if (mAvdAgent != null) { + displayValue(c, "Name:", mAvdAgent.getAvd().getName()); + displayValue(c, "CPU/ABI:", mAvdAgent.getPrettyAbiType()); + displayValue(c, "Path:", mAvdAgent.getPath()); + if (mAvdAgent.getAvd().getStatus() != AvdStatus.OK) { + displayValue(c, "Error:", mAvdAgent.getAvd().getErrorMessage()); + } else { + displayValue(c, "Target:", mAvdAgent.getTargetDisplayName()); + displayValue(c, "Skin:", mAvdAgent.getSkin()); + String sdcard = mAvdAgent.getSdcard(); + if (!sdcard.isEmpty()) { + displayValue(c, "SD Card:", sdcard); + } + String snapshot = mAvdAgent.getSnapshot(); + if (!snapshot.isEmpty()) { + displayValue(c, "Snapshot:", snapshot); + } + // display other hardware + HashMap copy = new HashMap(mAvdAgent.getAvd().getProperties()); + // remove stuff we already displayed (or that we don't want to display) + copy.remove(AvdManager.AVD_INI_ABI_TYPE); + copy.remove(AvdManager.AVD_INI_CPU_ARCH); + copy.remove(AvdManager.AVD_INI_SKIN_NAME); + copy.remove(AvdManager.AVD_INI_SKIN_PATH); + copy.remove(AvdManager.AVD_INI_SDCARD_SIZE); + copy.remove(AvdManager.AVD_INI_SDCARD_PATH); + copy.remove(AvdManager.AVD_INI_IMAGES_1); + copy.remove(AvdManager.AVD_INI_IMAGES_2); + + if (copy.size() > 0) { + Label l = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData( + GridData.FILL, GridData.HORIZONTAL_ALIGN_BEGINNING, false, false, 2, 1)); + //display = l.getDisplay(); + //l.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + c = new Composite(shell, SWT.NONE); + c.setLayout(gl = new GridLayout(2, false)); + //display = c.getDisplay(); + gl.marginHeight = gl.marginWidth = 0; + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + //c.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + for (Map.Entry entry : copy.entrySet()) { + displayValue(c, entry.getKey() + ":", entry.getValue()); + } + } + } + } + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + + @Override + protected void postCreate() { + // pass + } + + /** + * Displays a value with a label. + * + * @param parent the parent Composite in which to display the value. This Composite must use a + * {@link GridLayout} with 2 columns. + * @param label the label of the value to display. + * @param value the string value to display. + */ + private void displayValue(Composite parent, String key, String value) { + Label label = new Label(parent, SWT.LEFT); + label.setLayoutData(new GridData(GridData.FILL, GridData.VERTICAL_ALIGN_CENTER, false, false)); + Display display = label.getDisplay(); + if ((row & 1) == 0) { + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + } + label.setText(key); + + label = new Label(parent, SWT.LEFT); + label.setLayoutData(new GridData(GridData.FILL, GridData.VERTICAL_ALIGN_CENTER, true, false)); + display = label.getDisplay(); + if ((row & 1) == 0) { + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + } + label.setText(value); + ++row; + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java new file mode 100644 index 00000000..ff9b34d1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java @@ -0,0 +1,1246 @@ +/* + * 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.sdkuilib.internal.widgets; + +import java.awt.DisplayMode; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Formatter; +import java.util.Locale; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.DecorationOverlayIcon; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.repository.io.FileOp; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.repository.avd.SystemImageInfo; +import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; +import com.android.sdkuilib.internal.tasks.ProgressTask; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.ui.AvdDisplayMode; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.widgets.MessageBoxLog; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.android.utils.ILogger; +import com.android.utils.NullLogger; + + +/** + * The AVD selector is a table that is added to the given parent composite. + *

+ * After using one of the constructors, call {@link #setSelection(AvdInfo)}, + * {@link #setSelectionListener(SelectionListener)} and finally use + * {@link #getSelected()} to retrieve the selection. + */ +public final class AvdSelector { + private static final String STARTING_EMULATOR = "Starting Android Emulator"; + + private static int NUM_COL = 2; + + private final AvdDisplayMode mDisplayMode; + private final SdkTargets mSdkTargets; + private AvdManager mAvdManager; + private Table mTable; + private Button mDeleteButton; + private Button mDetailsButton; + private Button mNewButton; + private Button mEditButton; + private Button mRefreshButton; + private Button mManagerButton; + private Button mRepairButton; + private Button mStartButton; + private Button mOkButton; + + private SelectionListener mSelectionListener; + private IAvdFilter mTargetFilter; + + /** Defaults to true. Changed by the {@link #setEnabled(boolean)} method to represent the + * "global" enabled state on this composite. */ + private boolean mIsEnabled = true; + + private ImageFactory mImageFactory; + private Image mBrokenImage; + private Image mInvalidImage; + private final SdkContext mSdkContext; + + private boolean mInternalRefresh; + + /** + * A filter to control the whether or not an AVD should be displayed by the AVD Selector. + */ + public interface IAvdFilter { + /** + * Called before {@link #accept(AvdInfo)} is called for any AVD. + */ + void prepare(); + + /** + * Called to decided whether an AVD should be displayed. + * @param avdAgent Agent containing the AVD to test. + * @return true if the AVD should be displayed. + */ + boolean accept(AvdAgent avdAgent); + + /** + * Called after {@link #accept(AvdInfo)} has been called on all the AVDs. + */ + void cleanup(); + } + + /** + * Internal implementation of {@link IAvdFilter} to filter out the AVDs that are not + * running an image compatible with a specific target. + */ + private final static class TargetBasedFilter implements IAvdFilter { + private final IAndroidTarget mTarget; + + TargetBasedFilter(IAndroidTarget target) { + mTarget = target; + } + + @Override + public void prepare() { + // nothing to prepare + } + + @Override + public boolean accept(AvdAgent avdAgent) { + if (avdAgent != null) { + return mTarget.canRunOn(avdAgent.getTarget()); + } + + return false; + } + + @Override + public void cleanup() { + // nothing to clean up + } + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by a {@link IAndroidTarget}. + *

Only the {@link AvdInfo} able to run application developed for the given + * {@link IAndroidTarget} will be displayed. + * + * @param parent The parent composite where the selector will be added. + * @param sdkContext SDK handler and repo manager + * @param manager the AVD manager. + * @param filter When non-null, will allow filtering the AVDs to display. + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + SdkContext sdkContext, + IAvdFilter filter, + AvdDisplayMode displayMode) { + mSdkTargets = new SdkTargets(sdkContext); + mSdkContext = sdkContext; + mAvdManager = sdkContext.getAvdManager(); + mTargetFilter = filter; + mDisplayMode = displayMode; + + // get some bitmaps. + mImageFactory = mSdkContext.getSdkHelper().getImageFactory(); + mBrokenImage = mImageFactory.getImageByName("warning_icon16.png"); + mInvalidImage = mImageFactory.getImageByName("reject_icon16.png"); + + // Layout has 2 columns + Composite group = new Composite(parent, SWT.NONE); + GridLayout gl; + group.setLayout(gl = new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + + int style = SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER; + if (displayMode == AvdDisplayMode.SIMPLE_CHECK) { + style |= SWT.CHECK; + } + mTable = new Table(group, style); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + setTableHeightHint(0); + + Composite buttons = new Composite(group, SWT.NONE); + buttons.setLayout(gl = new GridLayout(1, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + buttons.setFont(group.getFont()); + + if (displayMode == AvdDisplayMode.MANAGER) { + mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewButton.setText("Create..."); + mNewButton.setToolTipText("Creates a new AVD."); + mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onNew(); + } + }); + } + + + mStartButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mStartButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStartButton.setText("Start..."); + mStartButton.setToolTipText("Starts the selected AVD."); + mStartButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onStart(); + } + }); + + @SuppressWarnings("unused") + Label spacing = new Label(buttons, SWT.NONE); + + if (displayMode == AvdDisplayMode.MANAGER) { + mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mEditButton.setText("Edit..."); + mEditButton.setToolTipText("Edit an existing AVD."); + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onEdit(); + } + }); + + mRepairButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRepairButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRepairButton.setText("Repair..."); + mRepairButton.setToolTipText("Repairs the selected AVD."); + mRepairButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onRepair(); + } + }); + + mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDeleteButton.setText("Delete..."); + mDeleteButton.setToolTipText("Deletes the selected AVD."); + mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDelete(); + } + }); + } + + mDetailsButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDetailsButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDetailsButton.setText("Details..."); + mDetailsButton.setToolTipText("Displays details of the selected AVD."); + mDetailsButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDetails(); + } + }); + + Composite padding = new Composite(buttons, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRefreshButton.setText("Refresh"); + mRefreshButton.setToolTipText("Reloads the list of AVD.\nUse this if you create AVDs from the command line."); + mRefreshButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + refresh(true); + } + }); + + if (displayMode != AvdDisplayMode.MANAGER) { + mManagerButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mManagerButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mManagerButton.setText("Manager..."); + mManagerButton.setToolTipText("Launches the AVD manager."); + mManagerButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onAvdManager(); + } + }); + addOkButton(buttons, parent.getShell()); + } else { + addOkButton(buttons, parent.getShell()); + Composite legend = new Composite(group, SWT.NONE); + legend.setLayout(gl = new GridLayout(4, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + legend.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false, + NUM_COL, 1)); + legend.setFont(group.getFont()); + + new Label(legend, SWT.NONE).setImage(mBrokenImage); + new Label(legend, SWT.NONE).setText("A repairable Android Virtual Device."); + new Label(legend, SWT.NONE).setImage(mInvalidImage); + new Label(legend, SWT.NONE).setText("An Android Virtual Device that failed to load. Click 'Details' to see the error."); + } + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("AVD Name"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Target Name"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("Platform"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("API Level"); + final TableColumn column4 = new TableColumn(mTable, SWT.NONE); + column4.setText("CPU/ABI"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3, column4); + setupSelectionListener(mTable); + fillTable(mTable); + setEnabled(true); + } + + private void addOkButton(Composite buttons, Shell shell) { + mOkButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mOkButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mOkButton.setText("OK"); + mOkButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + shell.close(); + } + }); + shell.setDefaultButton(mOkButton); + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}. + * + * @param parent The parent composite where the selector will be added. + * @param manager the AVD manager. + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + SdkContext sdkContext, + AvdDisplayMode displayMode) { + this(parent, sdkContext, (IAvdFilter)null /* filter */, displayMode); + } + + /** + * *** NOT REFERENCED *** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by an {@link IAndroidTarget}. + *

Only the {@link AvdInfo} able to run applications developed for the given + * {@link IAndroidTarget} will be displayed. + * + * @param parent The parent composite where the selector will be added. + * @param manager the AVD manager. + * @param filter Only shows the AVDs matching this target (must not be null). + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + SdkContext sdkContext, + IAndroidTarget filter, + AvdDisplayMode displayMode) { + this(parent, sdkContext, new TargetBasedFilter(filter), displayMode); + } + + public SdkTargets getSdkTargets() { + return mSdkTargets; + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + if (heightHint > 0) { + data.heightHint = heightHint; + } + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + /** + * Refresh the display of Android Virtual Devices. + * Tries to keep the selection. + *

+ * This must be called from the UI thread. + * + * @param reload if true, the AVD manager will reload the AVD from the disk. + * @return false if the reloading failed. This is always true if reload is + * false. + */ + public boolean refresh(boolean reload) { + if (!mInternalRefresh) { + try { + // Note that AvdManagerPage.onDevicesChange() will trigger a + // refresh while the AVDs are being reloaded so prevent from + // having a recursive call to here. + mInternalRefresh = true; + if (reload) { + try { + mAvdManager.reloadAvds(NullLogger.getLogger()); + } catch (AndroidLocationException e) { + return false; + } + } + + AvdAgent selected = getSelected(); + fillTable(mTable); + setSelection(selected); + return true; + } finally { + mInternalRefresh = false; + } + } + return false; + } + + /** + * Sets a new AVD manager + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param manager the AVD manager. + */ + public void setManager(AvdManager manager) { + mAvdManager = manager; + } + + /** + * Sets a new AVD filter. + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param filter An IAvdFilter. If non-null, this will filter out the AVD to not display. + */ + public void setFilter(IAvdFilter filter) { + mTargetFilter = filter; + } + + /** + * Sets a new Android Target-based AVD filter. + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param target An IAndroidTarget. If non-null, only AVD whose target are compatible with the + * filter target will displayed an available for selection. + */ + public void setFilter(IAndroidTarget target) { + if (target != null) { + mTargetFilter = new TargetBasedFilter(target); + } else { + mTargetFilter = null; + } + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + *

+ * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to + * display the details of the selected AVD.
+ * To disable it (when you provide your own double click action), set + * {@link SelectionEvent#doit} to false in + * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public void setSelection(AvdInfo avd) { + AvdAgent avdAgent = avdAgentInstance(avd); + if (avdAgent != null) + setSelection(avdAgent); + // Ignore AVD if there is no system image and Android version not supported + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(AvdAgent target) { + boolean found = false; + boolean modified = false; + + int selIndex = mTable.getSelectionIndex(); + int index = 0; + for (TableItem i : mTable.getItems()) { + AvdAgent avdAgent = (AvdAgent)i.getData(); + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { + if (avdAgent == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } else { + if (avdAgent == target) { + found = true; + if (index != selIndex) { + mTable.setSelection(index); + modified = true; + } + break; + } + + index++; + } + } + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + enableActionButtons(); + return found; + } + + /** + * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will + * return the {@link AvdInfo} that is checked instead of the list selection. + * + * @return The currently selected item or null. + */ + public AvdAgent getSelected() { + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (AvdAgent) i.getData(); + } + } + } else { + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (AvdAgent) mTable.getItem(selIndex).getData(); + } + } + return null; + } + + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + // We can only enable widgets if the AVD Manager is defined. + mIsEnabled = enabled && mAvdManager != null; + + mTable.setEnabled(mIsEnabled); + mRefreshButton.setEnabled(mIsEnabled); + + if (mNewButton != null) { + mNewButton.setEnabled(mIsEnabled); + } + if (mManagerButton != null) { + mManagerButton.setEnabled(mIsEnabled); + } + + enableActionButtons(); + } + + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3, + final TableColumn column4) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 20 / 100); // 20% + column1.setWidth(r.width * 30 / 100); // 30% + column2.setWidth(r.width * 10 / 100); // 10% + column3.setWidth(r.width * 10 / 100); // 10% + column4.setWidth(r.width * 30 / 100); // 30% + } + }); + } + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + + /** + * Handles single-click selection on the table. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + + enableActionButtons(); + } + + /** + * Handles double-click selection on the table. + * Note that the single-click handler will probably already have been called. + * + * On double-click, always check the table item. + * + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { + i.setChecked(true); + } + enforceSingleSelection(i); + + } + + // whether or not we display details. default: true when not in SIMPLE_CHECK mode. + boolean showDetails = mDisplayMode != AvdDisplayMode.SIMPLE_CHECK; + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + showDetails &= e.doit; // enforce false in SIMPLE_CHECK + } + + if (showDetails) { + onDetails(); + } + + enableActionButtons(); + } + + /** + * To ensure single selection, uncheck all other items when this one is selected. + * This makes the chekboxes act as radio buttons. + */ + private void enforceSingleSelection(TableItem item) { + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } else { + // pass + } + } + }); + } + + /** + * Fills the table with all AVD. + * The table columns are: + *

    + *
  • column 0: sdk name + *
  • column 1: sdk target + *
  • column 2: sdk platform + *
  • column 3: sdk API level + *
  • column 4: CPU/ABI + *
+ */ + private void fillTable(final Table table) { + table.removeAll(); + + // get the AVDs + AvdInfo avds[] = null; + if (mAvdManager != null) { + if (mDisplayMode == AvdDisplayMode.MANAGER) { + avds = mAvdManager.getAllAvds(); + } else { + avds = mAvdManager.getValidAvds(); + } + } + if (avds != null && avds.length > 0) { + Arrays.sort(avds, new Comparator() { + @Override + public int compare(AvdInfo o1, AvdInfo o2) { + return o1.compareTo(o2); + } + }); + table.setEnabled(true); + if (mTargetFilter != null) { + mTargetFilter.prepare(); + } + for (AvdInfo avd : avds) { + AvdAgent avdAgent = avdAgentInstance(avd); + if (avdAgent == null) + continue; // Ignore AVD if there is no system image and Android version not supported + if (mTargetFilter == null || mTargetFilter.accept(avdAgent)) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(avdAgent); + item.setText(0, avd.getName()); + if (mDisplayMode == AvdDisplayMode.MANAGER) { + AvdStatus status = avd.getStatus(); + + boolean isOk = status == AvdStatus.OK; + boolean isRepair = isAvdRepairable(status); + boolean isInvalid = !isOk && !isRepair; + + Image img = getTagImage(avd.getTag(), isOk, isRepair, isInvalid); + item.setImage(0, img); + } + item.setText(1, avdAgent.getTargetFullName()); + item.setText(2, avdAgent.getTargetVersionName()); + item.setText(3, avd.getAndroidVersion().getApiString()); + item.setText(4, AvdInfo.getPrettyAbiType(avd)); + } + } + if (mTargetFilter != null) { + mTargetFilter.cleanup(); + } + } + if (table.getItemCount() == 0) { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No AVD available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + private AvdAgent avdAgentInstance(AvdInfo avd) { + AvdAgent avdAgent = null; + SystemImageInfo systemImageInfo = new SystemImageInfo(avd); + if (systemImageInfo.hasSystemImage()) + avdAgent = new AvdAgent(mSdkTargets.getTargetForSysImage(systemImageInfo.getSystemImage()), avd); + else { + IAndroidTarget target = mSdkTargets.getTargetForAndroidVersion(avd.getAndroidVersion()); + if (target != null) + avdAgent = new AvdAgent(target, avd); + // Return null if there is no system image and Android version not supported + } + return avdAgent; + } + + @NonNull + private Image getTagImage(IdDisplay tag, + final boolean isOk, + final boolean isRepair, + final boolean isInvalid) { + if (tag == null) { + tag = SystemImage.DEFAULT_TAG; + } + + String fname = String.format("tag_%s_32.png", tag.getId()); + String kname = String.format("%d%d%d_%s", (isOk ? 1 : 0), + (isRepair ? 1 : 0), + (isInvalid ? 1 : 0), + fname); + if (isOk) + return mImageFactory.getImageByName(fname); + + return mImageFactory.getImageByName(fname, kname, new ImageFactory.ImageEditor() { + @Override + public ImageData edit(Image source) { + Image overlayImg = isRepair ? mBrokenImage : mInvalidImage; + ImageDescriptor overlayDesc = ImageDescriptor.createFromImage(overlayImg); + + DecorationOverlayIcon overlaid = + new DecorationOverlayIcon(source, overlayDesc, IDecoration.BOTTOM_RIGHT); + return overlaid.getImageData(); + } + }); + } + + /** + * Returns the currently selected AVD in the table. + *

+ * Unlike {@link #getSelected()} this will always return the item being selected + * in the list, ignoring the check boxes state in {@link DisplayMode#SIMPLE_CHECK} mode. + */ + private AvdAgent getTableSelection() { + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (AvdAgent) mTable.getItem(selIndex).getData(); + } + return null; + } + + /** + * Updates the enable state of the Details, Start, Delete and Update buttons. + */ + private void enableActionButtons() { + if (mIsEnabled == false) { + mDetailsButton.setEnabled(false); + mStartButton.setEnabled(false); + + if (mEditButton != null) { + mEditButton.setEnabled(false); + } + if (mDeleteButton != null) { + mDeleteButton.setEnabled(false); + } + if (mRepairButton != null) { + mRepairButton.setEnabled(false); + } + } else { + AvdAgent selection = getTableSelection(); + boolean hasSelection = selection != null; + + mDetailsButton.setEnabled(hasSelection); + mStartButton.setEnabled(hasSelection && + selection.getAvd().getStatus() == AvdStatus.OK); + + if (mEditButton != null) { + mEditButton.setEnabled(hasSelection); + } + if (mDeleteButton != null) { + mDeleteButton.setEnabled(hasSelection); + } + if (mRepairButton != null) { + mRepairButton.setEnabled(hasSelection && isAvdRepairable(selection.getAvd().getStatus())); + } + } + } + + private void onNew() { + AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), + mSdkContext, + mSdkTargets, + null); + + if (dlg.open() == Window.OK) { + refresh(false); //reload + } + } + + private void onEdit() { + AvdAgent avdAgent = getTableSelection(); + GridDialog dlg = null; + if (!avdAgent.getAvd().getDeviceName().isEmpty()) { + dlg = new AvdCreationDialog(mTable.getShell(), + mSdkContext, + mSdkTargets, + avdAgent); + } else { + // create a dialog with ok button and a warning icon + MessageBox dialog = + new MessageBox(mTable.getShell(), SWT.ICON_WARNING| SWT.OK); + dialog.setText("Legacy device not supported"); + dialog.setMessage(avdAgent.getAvd().getName() + " has is assigned a legacy device no longer supported by the Android SDK"); + // open dialog and await user selection + dialog.open(); + } + if ((dlg != null) && (dlg.open() == Window.OK)) { + refresh(false); //reload + } + } + + private void onDetails() { + AvdAgent avdAgent = getTableSelection(); + + AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdAgent); + dlg.open(); + } + + private void onDelete() { + final AvdAgent avdAgent = getTableSelection(); + + // get the current Display + final Display display = mTable.getDisplay(); + + // check if the AVD is running + if (mAvdManager.isAvdRunning(avdAgent.getAvd(), mSdkContext.getSdkLog())) { + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, + "Delete Android Virtual Device", + String.format( + "The Android Virtual Device '%1$s' is currently running in an emulator and cannot be deleted.", + avdAgent.getAvd().getName())); + } + }); + return; + } + + // Confirm you want to delete this AVD + final boolean[] result = new boolean[1]; + display.syncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + result[0] = MessageDialog.openQuestion(shell, + "Delete Android Virtual Device", + String.format( + "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.", + avdAgent.getAvd().getName())); + } + }); + + if (result[0] == false) { + return; + } + + ILogger log = null; + // log for this action. + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog( + String.format("Result of deleting AVD '%s':", avdAgent.getAvd().getName()), + display, + false /*logErrorsOnly*/); + } + + // delete the AVD + boolean success = mAvdManager.deleteAvd(avdAgent.getAvd(), log); + + // display the result + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(success); + } + + if (success) { + refresh(false /*reload*/); + } + } + + /** + * Repairs the selected AVD. + *

+ * For now this only supports fixing the wrong value in image.sysdir.* + */ + private void onRepair() { + final AvdAgent avdAgent = getTableSelection(); + + // get the current Display + final Display display = mTable.getDisplay(); + + ILogger log = null; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog( + String.format("Result of updating AVD '%s':", avdAgent.getAvd().getName()), + display, + false /*logErrorsOnly*/); + } + + if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_IMAGE_DIR) { + // delete the AVD + try { + mAvdManager.updateAvd(avdAgent.getAvd(), avdAgent.getAvd().getProperties()); + } catch (IOException e) { + log.error(e, null); + } + refresh(false /*reload*/); + } else if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_DEVICE_CHANGED) { + try { + mAvdManager.updateDeviceChanged(avdAgent.getAvd(), log); + } catch (IOException e) { + log.error(e, null); + } + refresh(false /*reload*/); + } else if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_DEVICE_MISSING) { + onEdit(); + } + } + + private void onAvdManager() { + + // get the current Display + Display display = mTable.getDisplay(); + + // log for this action. + ILogger log = null; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog("Result of SDK Manager", display, true /*logErrorsOnly*/); + } + + try { + AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( + mTable.getShell(), + log, + mSdkContext, + AvdInvocationContext.DIALOG); + + win.open(); + } catch (Exception ignore) {} + + refresh(true /*reload*/); // UpdaterWindow uses its own AVD manager so this one must reload. + + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(true); + } + } + + private void onStart() { + AvdAgent avdAgent = getTableSelection(); + + if (avdAgent == null) { + return; + } + File osSdkPath = mSdkContext.getLocation(); + AvdStartDialog dialog = new AvdStartDialog( + mTable.getShell(), + avdAgent, + osSdkPath, + mSdkContext.getSdkLog()); + if (dialog.open() == Window.OK) { + File path = new File(osSdkPath, SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_EMULATOR); + FileOp fileOp = mSdkContext.getFileOp(); + if (!fileOp.canExecute(path)) { + MessageDialog.openError(mTable.getShell(), STARTING_EMULATOR, "Cannot run emulator \"" + path.getAbsolutePath() + "\""); + return; + } + final String avdName = avdAgent.getAvd().getName(); + + // build the command line based on the available parameters. + ArrayList list = new ArrayList(); + list.add(path.getAbsolutePath()); + list.add("-avd"); //$NON-NLS-1$ + list.add(avdName); + if (dialog.hasWipeData()) { + list.add("-wipe-data"); //$NON-NLS-1$ + } + if (dialog.hasSnapshot()) { + if (!dialog.hasSnapshotLaunch()) { + list.add("-no-snapshot-load"); + } + if (!dialog.hasSnapshotSave()) { + list.add("-no-snapshot-save"); + } + } + float scale = dialog.getScale(); + if (scale != 0.f) { + // do the rounding ourselves. This is because %.1f will write .4899 as .4 + scale = Math.round(scale * 100); + scale /= 100.f; + list.add("-scale"); //$NON-NLS-1$ + // because the emulator expects English decimal values, don't use String.format + // but a Formatter. + Formatter formatter = new Formatter(Locale.US); + formatter.format("%.2f", scale); //$NON-NLS-1$ + list.add(formatter.toString()); + formatter.close(); + } + + // convert the list into an array for the call to exec. + final String[] command = list.toArray(new String[list.size()]); + + // launch the emulator + final ProgressTask progress = new ProgressTask(mTable.getShell(), + STARTING_EMULATOR); + progress.start(new ITask() { + volatile ITaskMonitor mMonitor = null; + + @Override + public void run(final ITaskMonitor monitor) { + mMonitor = monitor; + try { + monitor.setDescription( + "Starting emulator for AVD '%1$s'", + avdName); + monitor.log("Starting emulator for AVD '%1$s'", avdName); + + // we'll wait 100ms*100 = 10s. The emulator sometimes seem to + // start mostly OK just to crash a few seconds later. 10 seconds + // seems a good wait for that case. + int n = 100; + monitor.setProgressMax(n); + + Process process = Runtime.getRuntime().exec(command); + GrabProcessOutput.grabProcessOutput( + process, + Wait.ASYNC, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + filterStdOut(line); + } + + @Override + public void err(@Nullable String line) { + filterStdErr(line); + } + }); + + // This small wait prevents the dialog from closing too fast: + // When it works, the emulator returns immediately, even if + // no UI is shown yet. And when it fails (because the AVD is + // locked/running) this allows us to have time to capture the + // error and display it. + for (int i = 0; i < n; i++) { + try { + Thread.sleep(100); + monitor.incProgress(1); + } catch (InterruptedException e) { + // ignore + } + } + } catch (Exception e) { + monitor.logError("Failed to start emulator: %1$s", + e.getMessage()); + } finally { + mMonitor = null; + } + } + + private void filterStdOut(String line) { + ITaskMonitor m = mMonitor; + if (line == null || m == null) { + return; + } + + // Skip some non-useful messages. + if (line.indexOf("NSQuickDrawView") != -1) { //$NON-NLS-1$ + // Discard the MacOS warning: + // "This application, or a library it uses, is using NSQuickDrawView, + // which has been deprecated. Apps should cease use of QuickDraw and move + // to Quartz." + return; + } + + if (line.toLowerCase().indexOf("error") != -1 || //$NON-NLS-1$ + line.indexOf("qemu: fatal") != -1) { //$NON-NLS-1$ + // Sometimes the emulator seems to output errors on stdout. Catch these. + m.logError("%1$s", line); //$NON-NLS-1$ + return; + } + + m.log("%1$s", line); //$NON-NLS-1$ + } + + private void filterStdErr(String line) { + ITaskMonitor m = mMonitor; + if (line == null || m == null) { + return; + } + + if (line.indexOf("emulator: device") != -1 || //$NON-NLS-1$ + line.indexOf("HAX is working") != -1) { //$NON-NLS-1$ + // These are not errors. Output them as regular stdout messages. + m.log("%1$s", line); //$NON-NLS-1$ + return; + } + + m.logError("%1$s", line); //$NON-NLS-1$ + } + }); + } + } + + private boolean isAvdRepairable(AvdStatus avdStatus) { + return avdStatus == AvdStatus.ERROR_IMAGE_DIR + || avdStatus == AvdStatus.ERROR_DEVICE_CHANGED + || avdStatus == AvdStatus.ERROR_DEVICE_MISSING; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java new file mode 100644 index 00000000..a8dfe645 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java @@ -0,0 +1,631 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.ui.ResolutionChooserDialog; +import com.android.utils.ILogger; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.window.Window; +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.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.awt.Toolkit; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Dialog dealing with emulator launch options. The following options are supported: + *

    + *
  • -wipe-data
  • + *
  • -scale
  • + *
+ * Values are stored (in the class as static field) to be reused while the app is still running. + * The Monitor dpi is stored in the settings if available. + */ +final class AvdStartDialog extends GridDialog { + // static field to reuse values during the same session. + private static boolean sWipeData = false; + private static boolean sSnapshotSave = true; + private static boolean sSnapshotLaunch = true; + private static int sMonitorDpi = 72; // used if there's no setting controller. + private static final Map sSkinScaling = new HashMap(); + + private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?"); + + private final AvdInfo mAvd; + private final File mSdkLocation; + private final DeviceManager mDeviceManager; + + private Text mScreenSize; + private Text mMonitorDpi; + private Button mScaleButton; + + private float mScale = 0.f; + private boolean mWipeData = false; + private int mDensity = 160; // medium density + private int mSize1 = -1; + private int mSize2 = -1; + private String mSkinDisplay; + private boolean mEnableScaling = true; + private Label mScaleField; + private boolean mHasSnapshot = true; + private boolean mSnapshotSave = true; + private boolean mSnapshotLaunch = true; + private Button mSnapshotLaunchCheckbox; + + AvdStartDialog(Shell parentShell, AvdAgent avdAgent, File sdkLocation, ILogger sdkLog) { + super(parentShell, 2, false); + mAvd = avdAgent.getAvd(); + mSdkLocation = sdkLocation; + mDeviceManager = DeviceManager.createInstance(mSdkLocation, sdkLog); + if (mAvd == null) { + throw new IllegalArgumentException("avd cannot be null"); + } + if (mSdkLocation == null) { + throw new IllegalArgumentException("sdkLocation cannot be null"); + } + + computeSkinData(); + } + + public boolean hasWipeData() { + return mWipeData; + } + + /** + * Returns the scaling factor, or 0.f if none are set. + */ + public float getScale() { + return mScale; + } + + @Override + public void createDialogContent(final Composite parent) { + GridData gd; + + Label l = new Label(parent, SWT.NONE); + l.setText("Skin:"); + + l = new Label(parent, SWT.NONE); + l.setText(mSkinDisplay == null ? "None" : mSkinDisplay); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + l = new Label(parent, SWT.NONE); + l.setText("Density:"); + + l = new Label(parent, SWT.NONE); + l.setText(getDensityText()); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mScaleButton = new Button(parent, SWT.CHECK); + mScaleButton.setText("Scale display to real size"); + mScaleButton.setEnabled(mEnableScaling); + boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null; + mScaleButton.setSelection(defaultState); + mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + final Group scaleGroup = new Group(parent, SWT.NONE); + scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalIndent = 30; + gd.horizontalSpan = 2; + scaleGroup.setLayout(new GridLayout(3, false)); + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Screen Size (in):"); + mScreenSize = new Text(scaleGroup, SWT.BORDER); + mScreenSize.setText(getScreenSize()); + mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mScreenSize.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + // combine the current content and the new text + String text = mScreenSize.getText(); + text = text.substring(0, event.start) + event.text + text.substring(event.end); + + // now make sure it's a match for the regex + event.doit = sScreenSizePattern.matcher(text).matches(); + } + }); + mScreenSize.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent event) { + onScaleChange(); + } + }); + + // empty composite, only 2 widgets on this line. + new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData()); + gd.widthHint = gd.heightHint = 0; + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Monitor dpi:"); + mMonitorDpi = new Text(scaleGroup, SWT.BORDER); + mMonitorDpi.setText(Integer.toString(getMonitorDpi())); + mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.widthHint = 50; + mMonitorDpi.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + // check for digit only. + for (int i = 0 ; i < event.text.length(); i++) { + char letter = event.text.charAt(i); + if (letter < '0' || letter > '9') { + event.doit = false; + return; + } + } + } + }); + mMonitorDpi.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent event) { + onScaleChange(); + } + }); + + Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT); + button.setText("?"); + button.setToolTipText("Click to figure out your monitor's pixel density"); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell()); + if (dialog.open() == Window.OK) { + mMonitorDpi.setText(Integer.toString(dialog.getDensity())); + } + } + }); + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Scale:"); + mScaleField = new Label(scaleGroup, SWT.NONE); + mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, + true /*grabExcessHorizontalSpace*/, + true /*grabExcessVerticalSpace*/, + 2 /*horizontalSpan*/, + 1 /*verticalSpan*/)); + setScale(mScale); // set initial text value + + enableGroup(scaleGroup, defaultState); + + mScaleButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + boolean enabled = mScaleButton.getSelection(); + enableGroup(scaleGroup, enabled); + if (enabled) { + onScaleChange(); + } else { + setScale(0); + } + } + }); + + final Button wipeButton = new Button(parent, SWT.CHECK); + wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + wipeButton.setText("Wipe user data"); + wipeButton.setSelection(mWipeData = sWipeData); + wipeButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mWipeData = wipeButton.getSelection(); + updateSnapshotLaunchAvailability(); + } + }); + + Map prop = mAvd.getProperties(); + String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true"); + + mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK); + mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mSnapshotLaunchCheckbox.setText("Launch from snapshot"); + updateSnapshotLaunchAvailability(); + mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection(); + } + }); + + final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK); + snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + snapshotSaveCheckbox.setText("Save to snapshot"); + snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot); + snapshotSaveCheckbox.setEnabled(mHasSnapshot); + snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mSnapshotSave = snapshotSaveCheckbox.getSelection(); + } + }); + + l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + + // if the scaling is enabled by default, we must initialize the value of mScale + if (defaultState) { + onScaleChange(); + } + } + + /** On Windows we need to manually enable/disable the children of a group */ + private void enableGroup(final Group group, boolean enabled) { + group.setEnabled(enabled); + for (Control c : group.getChildren()) { + c.setEnabled(enabled); + } + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Launch Options"); + } + + @Override + protected Button createButton(Composite parent, int id, String label, boolean defaultButton) { + if (id == IDialogConstants.OK_ID) { + label = "Launch"; + } + + return super.createButton(parent, id, label, defaultButton); + } + + @Override + protected void okPressed() { + // override ok to store some info + // first the monitor dpi + String dpi = mMonitorDpi.getText(); + if (dpi.length() > 0) { + sMonitorDpi = Integer.parseInt(dpi); + } + + // now the scale factor + String key = mAvd.getName(); + sSkinScaling.remove(key); + if (mScaleButton.getSelection()) { + String size = mScreenSize.getText(); + if (size.length() > 0) { + sSkinScaling.put(key, size); + } + } + + // and then the wipe-data checkbox + sWipeData = mWipeData; + + // and the snapshot handling if those checkboxes are enabled. + if (mHasSnapshot) { + sSnapshotSave = mSnapshotSave; + if (!mWipeData) { + sSnapshotLaunch = mSnapshotLaunch; + } + } + + // finally continue with the ok action + super.okPressed(); + } + + private void computeSkinData() { + Map prop = mAvd.getProperties(); + String dpi = prop.get("hw.lcd.density"); + if (dpi != null && dpi.length() > 0) { + mDensity = Integer.parseInt(dpi); + } + + findSkinResolution(); + } + + private void onScaleChange() { + String sizeStr = mScreenSize.getText(); + if (sizeStr.length() == 0) { + setScale(0); + return; + } + + String dpiStr = mMonitorDpi.getText(); + if (dpiStr.length() == 0) { + setScale(0); + return; + } + + int dpi = Integer.parseInt(dpiStr); + + // The size number is formatted using String.format (locale formatting) + float size; + try { + size = sizeStr.isEmpty() ? 0.0f : NumberFormat.getNumberInstance().parse(sizeStr).floatValue(); + } catch (ParseException e) { + setScale(0); + return; + } + + /* + * We are trying to emulate the following device: + * resolution: 'mSize1'x'mSize2' + * density: 'mDensity' + * screen diagonal: 'size' + * ontop a monitor running at 'dpi' + */ + // We start by computing the screen diagonal in pixels, if the density was really mDensity + float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2); + // Now we would convert this in actual inches: + // diagonalIn = diagonal / mDensity + // the scale factor is a mix of adapting to the new density and to the new size. + // (size/diagonalIn) * (dpi/mDensity) + // this can be simplified to: + setScale((size * dpi) / diagonalPx); + } + + private void setScale(float scale) { + mScale = scale; + + // Do the rounding exactly like AvdSelector will do. + scale = Math.round(scale * 100); + scale /= 100.f; + + if (scale == 0.f) { + mScaleField.setText("default"); //$NON-NLS-1$ + } else { + mScaleField.setText(String.format(Locale.getDefault(), "%.2f", scale)); //$NON-NLS-1$ + } + } + + /** + * Returns the monitor dpi to start with. + * This can be coming from the settings, the session-based storage, or the from whatever Java + * can tell us. + */ + private int getMonitorDpi() { + + if (sMonitorDpi == -1) { // first time? try to get a value + sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); + } + + return sMonitorDpi; + } + + /** + * Returns the screen size to start with. + *

If an emulator with the same skin was already launched, scaled, the size used is reused. + *

If one hasn't been launched and the AVD is based on a device, use the device's screen + * size. Otherwise, use the default (3). + */ + private String getScreenSize() { + String size = sSkinScaling.get(mAvd.getName()); + if (size != null) { + return size; + } + + Map properties = mAvd.getProperties(); + if (properties != null) { + String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME); + String mfctr = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + if (name != null && mfctr != null) { + Device d = mDeviceManager.getDevice(name, mfctr); + if (d != null) { + double screenSize = + d.getDefaultHardware().getScreen().getDiagonalLength(); + return String.format(Locale.getDefault(), "%.1f", screenSize); + } + } + } + + return "3"; + } + + /** + * Returns a display string for the density. + */ + private String getDensityText() { + switch (mDensity) { + case 120: + return "Low (120)"; + case 160: + return "Medium (160)"; + case 240: + return "High (240)"; + } + + return Integer.toString(mDensity); + } + + /** + * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}. + */ + private void findSkinResolution() { + Map prop = mAvd.getProperties(); + String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME); + + if (skinName != null) { + Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName); + if (m != null && m.matches()) { + mSize1 = Integer.parseInt(m.group(1)); + mSize2 = Integer.parseInt(m.group(2)); + mSkinDisplay = skinName; + mEnableScaling = true; + return; + } + } + + // The resolution is inside the layout file of the skin. + mEnableScaling = false; // default to false for now. + + // path to the skin layout file. + String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH); + if (skinPath != null) { + File skinFolder = new File(mSdkLocation, skinPath); + if (skinFolder.isDirectory()) { + File layoutFile = new File(skinFolder, "layout"); + if (layoutFile.isFile()) { + if (parseLayoutFile(layoutFile)) { + mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2); + mEnableScaling = true; + } else { + mSkinDisplay = skinName; + } + } + } + } + } + + /** + * Parses a layout file. + *

+ * the format is relatively easy. It's a collection of items defined as + * ≶name> { + * ≶content> + * } + * + * content is either 1+ items or 1+ properties + * properties are defined as + * ≶name>≶whitespace>≶value> + * + * We're going to look for an item called display, with 2 properties height and width. + * This is very basic parser. + * + * @param layoutFile the file to parse + * @return true if both sizes where found. + */ + private boolean parseLayoutFile(File layoutFile) { + BufferedReader input = null; + try { + input = new BufferedReader(new FileReader(layoutFile)); + String line; + + while ((line = input.readLine()) != null) { + // trim to remove whitespace + line = line.trim(); + int len = line.length(); + if (len == 0) continue; + + // check if this is a new item + if (line.charAt(len-1) == '{') { + // this is the start of a node + String[] tokens = line.split(" "); + if ("display".equals(tokens[0])) { + // this is the one we're looking for! + while ((mSize1 == -1 || mSize2 == -1) && + (line = input.readLine()) != null) { + // trim to remove whitespace + line = line.trim(); + len = line.length(); + if (len == 0) continue; + + if ("}".equals(line)) { // looks like we're done with the item. + break; + } + + tokens = line.split(" "); + if (tokens.length >= 2) { + // there can be multiple space between the name and value + // in which case we'll get an extra empty token in the middle. + if ("width".equals(tokens[0])) { + mSize1 = Integer.parseInt(tokens[tokens.length-1]); + } else if ("height".equals(tokens[0])) { + mSize2 = Integer.parseInt(tokens[tokens.length-1]); + } + } + } + + return mSize1 != -1 && mSize2 != -1; + } + } + + } + // if it reaches here, display was not found. + // false is returned below. + } catch (IOException e) { + // ignore. + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + // ignore + } + } + } + + return false; + } + + /** + * @return Whether there's a snapshot file available. + */ + public boolean hasSnapshot() { + return mHasSnapshot; + } + + /** + * @return Whether to launch and load snapshot. + */ + public boolean hasSnapshotLaunch() { + return mSnapshotLaunch && !hasWipeData(); + } + + /** + * @return Whether to preserve emulator state to snapshot. + */ + public boolean hasSnapshotSave() { + return mSnapshotSave; + } + + /** + * Updates snapshot launch availability, for when mWipeData value changes. + */ + private void updateSnapshotLaunchAvailability() { + boolean enabled = !mWipeData && mHasSnapshot; + mSnapshotLaunchCheckbox.setEnabled(enabled); + mSnapshotLaunch = enabled && sSnapshotLaunch; + mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java new file mode 100644 index 00000000..bf482e25 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java @@ -0,0 +1,1086 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.annotations.Nullable; +import com.android.resources.Density; +import com.android.resources.Keyboard; +import com.android.resources.KeyboardState; +import com.android.resources.Navigation; +import com.android.resources.NavigationState; +import com.android.resources.ResourceEnum; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenRatio; +import com.android.resources.ScreenSize; +import com.android.resources.TouchScreen; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.devices.ButtonType; +import com.android.sdklib.devices.Camera; +import com.android.sdklib.devices.CameraLocation; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DeviceFilter; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Multitouch; +import com.android.sdklib.devices.Network; +import com.android.sdklib.devices.PowerType; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.ScreenType; +import com.android.sdklib.devices.Sensor; +import com.android.sdklib.devices.Software; +import com.android.sdklib.devices.State; +import com.android.sdklib.devices.Storage; +import org.eclipse.andmore.base.resources.ImageFactory; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.Collection; +import java.util.Map; + +public class DeviceCreationDialog extends GridDialog { + + private static final String MANUFACTURER = "User"; + + private final ImageFactory mImageFactory; + private final DeviceManager mManager; + private Collection mUserDevices; + + private Device mDevice; + + private Text mDeviceName; + private Text mDiagonalLength; + private Text mXDimension; + private Text mYDimension; + private Button mKeyboard; + private Button mDpad; + private Button mTrackball; + private Button mNoNav; + private Text mRam; + private Combo mRamCombo; + private Combo mButtons; + private Combo mSize; + private Combo mDensity; + private Combo mRatio; + private Button mAccelerometer; // hw.accelerometer + private Button mGyro; // hw.sensors.orientation + private Button mGps; // hw.sensors.gps + private Button mProximitySensor; // hw.sensors.proximity + private Button mCameraFront; + private Button mCameraRear; + private Group mStateGroup; + private Button mPortrait; + private Label mPortraitLabel; + private Button mPortraitNav; + private Button mLandscape; + private Label mLandscapeLabel; + private Button mLandscapeNav; + private Button mPortraitKeys; + private Label mPortraitKeysLabel; + private Button mPortraitKeysNav; + private Button mLandscapeKeys; + private Label mLandscapeKeysLabel; + private Button mLandscapeKeysNav; + + private Button mForceCreation; + private Label mStatusIcon; + private Label mStatusLabel; + + private Button mOkButton; + + /** The hardware instance attached to each of the states of the created device. */ + private Hardware mHardware; + /** The instance of the Device created by the dialog, if the user pressed {@code mOkButton}. */ + private Device mCreatedDevice; + + /** + * This contains the Software for the device. Since it has no effect on the + * emulator whatsoever, we just use a single instance with reasonable + * defaults. */ + private static final Software mSoftware; + + static { + mSoftware = new Software(); + mSoftware.setLiveWallpaperSupport(true); + mSoftware.setGlVersion("2.0"); + } + + public DeviceCreationDialog(Shell parentShell, + DeviceManager manager, + ImageFactory imageFactory, + @Nullable Device device) { + super(parentShell, 3, false); + mImageFactory = imageFactory; + mDevice = device; + mManager = manager; + mUserDevices = mManager.getDevices(DeviceFilter.USER); + } + + /** + * Returns the instance of the Device created by the dialog, + * if the user pressed the OK|create|edit|clone button. + * Typically only non-null if the dialog returns OK. + */ + public Device getCreatedDevice() { + return mCreatedDevice; + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + + mOkButton = getButton(IDialogConstants.OK_ID); + + if (mDevice == null) { + getShell().setText("Create New Device"); + } else { + if (mUserDevices.contains(mDevice)) { + getShell().setText("Edit Device"); + } else { + getShell().setText("Clone Device"); + } + } + + Object ld = mOkButton.getLayoutData(); + if (ld instanceof GridData) { + ((GridData) ld).widthHint = 100; + } + + validatePage(); + + return control; + } + + @Override + public void createDialogContent(Composite parent) { + + ValidationListener validator = new ValidationListener(); + SizeListener sizeListener = new SizeListener(); + NavStateListener navListener = new NavStateListener(); + + Composite column1 = new Composite(parent, SWT.NONE); + GridDataBuilder.create(column1).hFill().vTop(); + GridLayoutBuilder.create(column1).columns(2); + + // vertical separator between column 1 and 2 + Label label = new Label(parent, SWT.SEPARATOR | SWT.VERTICAL); + GridDataBuilder.create(label).vFill().vGrab(); + + Composite column2 = new Composite(parent, SWT.NONE); + GridDataBuilder.create(column2).hFill().vTop(); + GridLayoutBuilder.create(column2).columns(2); + + // Column 1 + + String tooltip = "Name of the new device"; + generateLabel("Name:", tooltip, column1); + mDeviceName = generateText(column1, tooltip, new CreateNameModifyListener()); + + tooltip = "Diagonal length of the screen in inches"; + generateLabel("Screen Size (in):", tooltip, column1); + mDiagonalLength = generateText(column1, tooltip, sizeListener); + + tooltip = "The resolution of the device in pixels"; + generateLabel("Resolution (px):", tooltip, column1); + Composite dimensionGroup = new Composite(column1, SWT.NONE); // Like a Group with no border + GridDataBuilder.create(dimensionGroup).hFill(); + GridLayoutBuilder.create(dimensionGroup).columns(3).noMargins(); + mXDimension = generateText(dimensionGroup, tooltip, sizeListener); + new Label(dimensionGroup, SWT.NONE).setText("x"); + mYDimension = generateText(dimensionGroup, tooltip, sizeListener); + + label = new Label(column1, SWT.None); // empty space holder + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + + // Column 2 + + tooltip = "The screen size bucket that the device falls into"; + generateLabel("Size:", tooltip, column2); + mSize = generateCombo(column2, tooltip, ScreenSize.values(), 1, validator); + + tooltip = "The aspect ratio bucket the screen falls into. A \"long\" screen is wider."; + generateLabel("Screen Ratio:", tooltip, column2); + mRatio = generateCombo(column2, tooltip, ScreenRatio.values(), 1, validator); + + tooltip = "The pixel density bucket the device falls in"; + generateLabel("Density:", tooltip, column2); + mDensity = generateCombo(column2, tooltip, Density.values(), 3, validator); + + label = new Label(column2, SWT.None); // empty space holder + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + + + // Column 1, second row + + generateLabel("Sensors:", "The sensors available on the device", column1); + Group sensorGroup = new Group(column1, SWT.NONE); + sensorGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sensorGroup.setLayout(new GridLayout(2, false)); + mAccelerometer = generateButton(sensorGroup, "Accelerometer", + "Presence of an accelerometer", SWT.CHECK, true, validator); + mGyro = generateButton(sensorGroup, "Gyroscope", + "Presence of a gyroscope", SWT.CHECK, true, validator); + mGps = generateButton(sensorGroup, "GPS", "Presence of a GPS", SWT.CHECK, true, validator); + mProximitySensor = generateButton(sensorGroup, "Proximity Sensor", + "Presence of a proximity sensor", SWT.CHECK, true, validator); + + generateLabel("Cameras", "The cameras available on the device", column1); + Group cameraGroup = new Group(column1, SWT.NONE); + cameraGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + cameraGroup.setLayout(new GridLayout(2, false)); + mCameraFront = generateButton(cameraGroup, "Front", "Presence of a front camera", + SWT.CHECK, false, validator); + mCameraRear = generateButton(cameraGroup, "Rear", "Presence of a rear camera", + SWT.CHECK, true, validator); + + generateLabel("Input:", "The input hardware on the given device", column1); + Group inputGroup = new Group(column1, SWT.NONE); + inputGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + inputGroup.setLayout(new GridLayout(3, false)); + mKeyboard = generateButton(inputGroup, "Keyboard", "Presence of a hardware keyboard", + SWT.CHECK, false, + new KeyboardListener()); + GridData gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 3; + mKeyboard.setLayoutData(gridData); + mNoNav = generateButton(inputGroup, "No Nav", "No hardware navigation", + SWT.RADIO, true, navListener); + mDpad = generateButton(inputGroup, "DPad", "The device has a DPad navigation element", + SWT.RADIO, false, navListener); + mTrackball = generateButton(inputGroup, "Trackball", + "The device has a trackball navigation element", SWT.RADIO, false, navListener); + + tooltip = "The amount of RAM on the device"; + generateLabel("RAM:", tooltip, column1); + Group ramGroup = new Group(column1, SWT.NONE); + ramGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + ramGroup.setLayout(new GridLayout(2, false)); + mRam = generateText(ramGroup, tooltip, validator); + mRamCombo = new Combo(ramGroup, SWT.DROP_DOWN | SWT.READ_ONLY); + mRamCombo.setToolTipText(tooltip); + mRamCombo.add("MiB"); + mRamCombo.add("GiB"); + mRamCombo.select(0); + mRamCombo.addModifyListener(validator); + + // Column 2, second row + + tooltip = "Type of buttons (Home, Menu, etc.) on the device. " + + "This can be software buttons like on the Galaxy Nexus, or hardware buttons like " + + "the capacitive buttons on the Nexus S."; + generateLabel("Buttons:", tooltip, column2); + mButtons = new Combo(column2, SWT.DROP_DOWN | SWT.READ_ONLY); + mButtons.setToolTipText(tooltip); + mButtons.add(ButtonType.SOFT.getDescription()); + mButtons.add(ButtonType.HARD.getDescription()); + mButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mButtons.select(0); + mButtons.addModifyListener(validator); + + generateLabel("Device States:", "The available states for the given device", column2); + + mStateGroup = new Group(column2, SWT.NONE); + mStateGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStateGroup.setLayout(new GridLayout(2, true)); + + tooltip = "The device has a portait position with no keyboard available"; + mPortraitLabel = generateLabel("Portrait:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mPortraitLabel.setLayoutData(gridData); + mPortrait = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mPortraitNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mPortraitNav.setEnabled(false); + + tooltip = "The device has a landscape position with no keyboard available"; + mLandscapeLabel = generateLabel("Landscape:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mLandscapeLabel.setLayoutData(gridData); + mLandscape = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mLandscapeNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mLandscapeNav.setEnabled(false); + + tooltip = "The device has a portait position with a keyboard available"; + mPortraitKeysLabel = generateLabel("Portrait with keyboard:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mPortraitKeysLabel.setLayoutData(gridData); + mPortraitKeysLabel.setEnabled(false); + mPortraitKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mPortraitKeys.setEnabled(false); + mPortraitKeysNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mPortraitKeysNav.setEnabled(false); + + tooltip = "The device has a landscape position with the keyboard open"; + mLandscapeKeysLabel = generateLabel("Landscape with keyboard:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mLandscapeKeysLabel.setLayoutData(gridData); + mLandscapeKeysLabel.setEnabled(false); + mLandscapeKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mLandscapeKeys.setEnabled(false); + mLandscapeKeysNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mLandscapeKeysNav.setEnabled(false); + + + mForceCreation = new Button(column2, SWT.CHECK); + mForceCreation.setText("Override the existing device with the same name"); + mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); + mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, + true, false, 2, 1)); + mForceCreation.setEnabled(false); + mForceCreation.addSelectionListener(validator); + + + // -- third row + + // add a separator to separate from the ok/cancel button + label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + + // add stuff for the error display + Composite statusComposite = new Composite(parent, SWT.NONE); + GridLayout gl; + statusComposite.setLayoutData( + new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + statusComposite.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mStatusIcon = new Label(statusComposite, SWT.NONE); + mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + mStatusLabel = new Label(statusComposite, SWT.NONE); + mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStatusLabel.setText(""); //$NON-NLS-1$ + + prefillWithDevice(mDevice); + + validatePage(); + } + + private Button generateButton(Composite parent, String text, String tooltip, int type, + boolean selected, SelectionListener listener) { + Button b = new Button(parent, type); + b.setText(text); + b.setToolTipText(tooltip); + b.setSelection(selected); + b.addSelectionListener(listener); + b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + return b; + } + + /** + * Generates a combo widget attached to the given parent, then sets the + * tooltip, adds all of the {@link String}s returned by + * {@link ResourceEnum#getResourceValue()} for each {@link ResourceEnum}, + * sets the combo to the given index and adds the given + * {@link ModifyListener}. + */ + private Combo generateCombo(Composite parent, String tooltip, ResourceEnum[] values, + int selection, + ModifyListener validator) { + Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + c.setToolTipText(tooltip); + for (ResourceEnum r : values) { + c.add(r.getResourceValue()); + } + c.select(selection); + c.addModifyListener(validator); + return c; + } + + /** Generates a text widget with the given tooltip, parent and listener */ + private Text generateText(Composite parent, String tooltip, ModifyListener listener) { + Text t = new Text(parent, SWT.BORDER); + t.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + t.setToolTipText(tooltip); + t.addModifyListener(listener); + return t; + } + + /** Generates a label and attaches it to the given parent */ + private Label generateLabel(String text, String tooltip, Composite parent) { + Label label = new Label(parent, SWT.NONE); + label.setText(text); + label.setToolTipText(tooltip); + label.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_CENTER)); + return label; + } + + /** + * Callback when the device name is changed. Enforces that device names + * don't conflict with already existing devices unless we're editing that + * device. + */ + private class CreateNameModifyListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + String name = mDeviceName.getText(); + boolean nameCollision = false; + for (Device d : mUserDevices) { + if (MANUFACTURER.equals(d.getManufacturer()) && name.equals(d.getName())) { + nameCollision = true; + break; + } + } + mForceCreation.setEnabled(nameCollision); + mForceCreation.setSelection(!nameCollision); + + validatePage(); + } + } + + /** + * Callback attached to the diagonal length and resolution text boxes. Sets + * the screen size and display density based on their values, then validates + * the page. + */ + private class SizeListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + + if (!mDiagonalLength.getText().isEmpty()) { + try { + double diagonal = Double.parseDouble(mDiagonalLength.getText()); + double diagonalDp = 160.0 * diagonal; + + // Set the Screen Size + if (diagonalDp >= 1200) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("xlarge"))); + } else if (diagonalDp >= 800) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("large"))); + } else if (diagonalDp >= 568) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("normal"))); + } else { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("small"))); + } + if (!mXDimension.getText().isEmpty() && !mYDimension.getText().isEmpty()) { + + // Set the density based on which bucket it's closest to + double x = Double.parseDouble(mXDimension.getText()); + double y = Double.parseDouble(mYDimension.getText()); + double dpi = Math.sqrt(x * x + y * y) / diagonal; + double difference = Double.MAX_VALUE; + Density bucket = Density.MEDIUM; + for (Density d : Density.values()) { + if (Math.abs(d.getDpiValue() - dpi) < difference) { + difference = Math.abs(d.getDpiValue() - dpi); + bucket = d; + } + } + mDensity.select(Density.getIndex(bucket)); + } + } catch (NumberFormatException ignore) {} + } + } + } + + + /** + * Callback attached to the keyboard checkbox.Enables / disables device + * states based on the keyboard presence and then validates the page. + */ + private class KeyboardListener extends SelectionAdapter { + @Override + public void widgetSelected(SelectionEvent event) { + super.widgetSelected(event); + if (mKeyboard.getSelection()) { + mPortraitKeys.setEnabled(true); + mPortraitKeysLabel.setEnabled(true); + mLandscapeKeys.setEnabled(true); + mLandscapeKeysLabel.setEnabled(true); + } else { + mPortraitKeys.setEnabled(false); + mPortraitKeysLabel.setEnabled(false); + mLandscapeKeys.setEnabled(false); + mLandscapeKeysLabel.setEnabled(false); + } + toggleNav(); + validatePage(); + } + + } + + /** + * Listens for changes on widgets that affect nav availability and toggles + * the nav checkboxes for device states based on them. + */ + private class NavStateListener extends SelectionAdapter { + @Override + public void widgetSelected(SelectionEvent event) { + super.widgetSelected(event); + toggleNav(); + validatePage(); + } + } + + /** + * Method that inspects all of the relevant dialog state and enables or disables the nav + * elements accordingly. + */ + private void toggleNav() { + mPortraitNav.setEnabled(mPortrait.getSelection() && !mNoNav.getSelection()); + mLandscapeNav.setEnabled(mLandscape.getSelection() && !mNoNav.getSelection()); + mPortraitKeysNav.setEnabled(mPortraitKeys.getSelection() && mPortraitKeys.getEnabled() + && !mNoNav.getSelection()); + mLandscapeKeysNav.setEnabled(mLandscapeKeys.getSelection() + && mLandscapeKeys.getEnabled() && !mNoNav.getSelection()); + validatePage(); + } + + /** + * Callback that validates the page on modification events or widget + * selections + */ + private class ValidationListener extends SelectionAdapter implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + validatePage(); + } + } + + /** + * Validates all of the config options to ensure a valid device can be + * created from them. + * + * @return Whether the config options will result in a valid device. + */ + private boolean validatePage() { + boolean valid = true; + String error = null; + String warning = null; + setError(null); + + String name = mDeviceName.getText(); + + /* If we're editing / cloning a device, this will get called when the name gets pre-filled + * but the ok button won't be populated yet, so we need to skip the initial setting. + */ + if (mOkButton != null) { + if (mDevice == null) { + getShell().setText("Create New Device"); + mOkButton.setText("Create Device"); + } else { + if (mDevice.getName().equals(name)){ + if (mUserDevices.contains(mDevice)) { + getShell().setText("Edit Device"); + mOkButton.setText("Edit Device"); + } else { + warning = "Only user created devices are editable.\nA clone of it will be created under " + + "the \"User\" category."; + getShell().setText("Clone Device"); + mOkButton.setText("Clone Device"); + } + } else { + warning = "The device \"" + mDevice.getName() +"\" will be duplicated into\n" + + "\"" + name + "\" under the \"User\" category"; + getShell().setText("Clone Device"); + mOkButton.setText("Clone Device"); + } + } + } + + if (valid && name.isEmpty()) { + warning = "Please enter a name for the device."; + valid = false; + } + if (valid && !validateFloat("Diagonal Length", mDiagonalLength.getText())) { + warning = "Please enter a screen size."; + valid = false; + } + if (valid && !validateInt("Resolution", mXDimension.getText())) { + warning = "Please enter the screen resolution."; + valid = false; + } + if (valid && !validateInt("Resolution", mYDimension.getText())) { + warning = "Please enter the screen resolution."; + valid = false; + } + if (valid && mSize.getSelectionIndex() < 0) { + error = "A size bucket must be selected."; + valid = false; + } + if (valid && mDensity.getSelectionIndex() < 0) { + error = "A screen density bucket must be selected"; + valid = false; + } + if (valid && mRatio.getSelectionIndex() < 0) { + error = "A screen ratio must be selected."; + valid = false; + } + if (valid && !mNoNav.getSelection() && !mTrackball.getSelection() && !mDpad.getSelection()) { + error = "A mode of hardware navigation, or no navigation, has to be selected."; + valid = false; + } + if (valid && !validateInt("RAM", mRam.getText())) { + warning = "Please enter a RAM amount."; + valid = false; + } + if (valid && mRamCombo.getSelectionIndex() < 0) { + error = "RAM must have a selected unit."; + valid = false; + } + if (valid && mButtons.getSelectionIndex() < 0) { + error = "A button type must be selected."; + valid = false; + } + if (valid) { + if (mKeyboard.getSelection()) { + if (!mPortraitKeys.getSelection() + && !mPortrait.getSelection() + && !mLandscapeKeys.getSelection() + && !mLandscape.getSelection()) { + error = "At least one device state must be enabled."; + valid = false; + } + } else { + if (!mPortrait.getSelection() && !mLandscape.getSelection()) { + error = "At least one device state must be enabled"; + valid = false; + } + } + } + if (mForceCreation.isEnabled() && !mForceCreation.getSelection()) { + error = "Name conflicts with an existing device."; + valid = false; + } + + if (mOkButton != null) { + mOkButton.setEnabled(valid); + } + + if (error != null) { + setError(error); + } else if (warning != null) { + setWarning(warning); + } + + return valid; + } + + /** + * Validates the string is a valid, positive float. If not, it sets the + * error at the bottom of the dialog and returns false. Note this does + * not unset the error message, it's up to the caller to unset it iff + * it knows there are no errors on the page. + */ + private boolean validateFloat(String box, String value) { + if (value == null || value.isEmpty()) { + return false; + } + boolean ret = true; + try { + double val = Double.parseDouble(value); + if (val <= 0) { + ret = false; + } + } catch (NumberFormatException e) { + ret = false; + } + if (!ret) { + setError(box + " must be a valid, positive number."); + } + return ret; + } + + /** + * Validates the string is a valid, positive integer. If not, it sets the + * error at the bottom of the dialog and returns false. Note this does + * not unset the error message, it's up to the caller to unset it iff + * it knows there are no errors on the page. + */ + private boolean validateInt(String box, String value) { + if (value == null || value.isEmpty()) { + return false; + } + boolean ret = true; + try { + int val = Integer.parseInt(value); + if (val <= 0) { + ret = false; + } + } catch (NumberFormatException e) { + ret = false; + } + + if (!ret) { + setError(box + " must be a valid, positive integer."); + } + + return ret; + } + + /** + * Sets the error to the given string. If null, removes the error message. + */ + private void setError(@Nullable String error) { + if (error == null) { + mStatusIcon.setImage(null); + mStatusLabel.setText(""); + } else { + mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png")); + mStatusLabel.setText(error); + } + } + + /** + * Sets the warning message to the given string. If null, removes the + * warning message. + */ + private void setWarning(@Nullable String warning) { + if (warning == null) { + mStatusIcon.setImage(null); + mStatusLabel.setText(""); + } else { + mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png")); + mStatusLabel.setText(warning); + } + } + + /** Sets the hardware for the new device */ + private void prefillWithDevice(@Nullable Device device) { + if (device == null) { + + // Setup the default hardware instance with reasonable values for + // the things which are configurable via this dialog. + mHardware = new Hardware(); + + Screen s = new Screen(); + s.setXdpi(316); + s.setYdpi(316); + s.setMultitouch(Multitouch.JAZZ_HANDS); + s.setMechanism(TouchScreen.FINGER); + s.setScreenType(ScreenType.CAPACITIVE); + mHardware.setScreen(s); + + mHardware.addNetwork(Network.BLUETOOTH); + mHardware.addNetwork(Network.WIFI); + mHardware.addNetwork(Network.NFC); + + mHardware.addSensor(Sensor.BAROMETER); + mHardware.addSensor(Sensor.COMPASS); + mHardware.addSensor(Sensor.LIGHT_SENSOR); + + mHardware.setHasMic(true); + mHardware.addInternalStorage(new Storage(4, Storage.Unit.GiB)); + mHardware.setCpu("Generic CPU"); + mHardware.setGpu("Generic GPU"); + + mHardware.addSupportedAbi(Abi.ARMEABI); + mHardware.addSupportedAbi(Abi.ARMEABI_V7A); + mHardware.addSupportedAbi(Abi.MIPS); + mHardware.addSupportedAbi(Abi.X86); + + mHardware.setChargeType(PowerType.BATTERY); + return; + } + mHardware = device.getDefaultHardware().deepCopy(); + mDeviceName.setText(device.getName()); + mForceCreation.setSelection(true); + Screen s = mHardware.getScreen(); + mDiagonalLength.setText(Double.toString(s.getDiagonalLength())); + mXDimension.setText(Integer.toString(s.getXDimension())); + mYDimension.setText(Integer.toString(s.getYDimension())); + String size = s.getSize().getResourceValue(); + for (int i = 0; i < mSize.getItemCount(); i++) { + if (size.equals(mSize.getItem(i))) { + mSize.select(i); + break; + } + } + String ratio = s.getRatio().getResourceValue(); + for (int i = 0; i < mRatio.getItemCount(); i++) { + if (ratio.equals(mRatio.getItem(i))) { + mRatio.select(i); + break; + } + } + String density = s.getPixelDensity().getResourceValue(); + for (int i = 0; i < mDensity.getItemCount(); i++) { + if (density.equals(mDensity.getItem(i))) { + mDensity.select(i); + break; + } + } + mKeyboard.setSelection(!Keyboard.NOKEY.equals(mHardware.getKeyboard())); + mDpad.setSelection(Navigation.DPAD.equals(mHardware.getNav())); + mTrackball.setSelection(Navigation.TRACKBALL.equals(mHardware.getNav())); + mNoNav.setSelection(Navigation.NONAV.equals(mHardware.getNav())); + mAccelerometer.setSelection(mHardware.getSensors().contains(Sensor.ACCELEROMETER)); + mGyro.setSelection(mHardware.getSensors().contains(Sensor.GYROSCOPE)); + mGps.setSelection(mHardware.getSensors().contains(Sensor.GPS)); + mProximitySensor.setSelection(mHardware.getSensors().contains(Sensor.PROXIMITY_SENSOR)); + mCameraFront.setSelection(false); + mCameraRear.setSelection(false); + for (Camera c : mHardware.getCameras()) { + if (CameraLocation.FRONT.equals(c.getLocation())) { + mCameraFront.setSelection(true); + } else if (CameraLocation.BACK.equals(c.getLocation())) { + mCameraRear.setSelection(true); + } + } + mRam.setText(Long.toString(mHardware.getRam().getSizeAsUnit(Storage.Unit.MiB))); + mRamCombo.select(0); + + for (int i = 0; i < mButtons.getItemCount(); i++) { + if (mButtons.getItem(i).equals(mHardware.getButtonType().getDescription())) { + mButtons.select(i); + break; + } + } + + for (State state : device.getAllStates()) { + Button nav = null; + if (state.getOrientation().equals(ScreenOrientation.PORTRAIT)) { + if (state.getKeyState().equals(KeyboardState.EXPOSED)) { + mPortraitKeys.setSelection(true); + nav = mPortraitKeysNav; + } else { + mPortrait.setSelection(true); + nav = mPortraitNav; + } + } else { + if (state.getKeyState().equals(KeyboardState.EXPOSED)) { + mLandscapeKeys.setSelection(true); + nav = mLandscapeKeysNav; + } else { + mLandscape.setSelection(true); + nav = mLandscapeNav; + } + } + nav.setSelection(state.getNavState().equals(NavigationState.EXPOSED) + && !mHardware.getNav().equals(Navigation.NONAV)); + } + } + + /** + * If given a valid page, generates the corresponding device. The device is + * then added to the user device list, replacing any previous device with + * its given name and manufacturer, and the list is saved out to disk. + */ + @Override + protected void okPressed() { + if (validatePage()) { + Device.Builder builder = new Device.Builder(); + builder.setManufacturer("User"); + builder.setName(mDeviceName.getText()); + + if (mDevice != null) { + builder.setTagId(mDevice.getTagId()); + for (Map.Entry entry : mDevice.getBootProps().entrySet()) { + builder.addBootProp(entry.getKey(), entry.getValue()); + } + } + + builder.addSoftware(mSoftware); + Screen s = mHardware.getScreen(); + double diagonal = Double.parseDouble(mDiagonalLength.getText()); + int x = Integer.parseInt(mXDimension.getText()); + int y = Integer.parseInt(mYDimension.getText()); + s.setDiagonalLength(diagonal); + s.setXDimension(x); + s.setYDimension(y); + // The diagonal DPI will be somewhere in between the X and Y dpi if + // they differ + double dpi = Math.sqrt(x * x + y * y) / diagonal; + // The diagonal DPI should keep only two digits precision. + dpi = Math.round(dpi * 100) / 100.0; + s.setXdpi(dpi); + s.setYdpi(dpi); + s.setPixelDensity(Density.getEnum(mDensity.getText())); + s.setSize(ScreenSize.getEnum(mSize.getText())); + s.setRatio(ScreenRatio.getEnum(mRatio.getText())); + if (mAccelerometer.getSelection()) { + mHardware.addSensor(Sensor.ACCELEROMETER); + } + if (mGyro.getSelection()) { + mHardware.addSensor(Sensor.GYROSCOPE); + } + if (mGps.getSelection()) { + mHardware.addSensor(Sensor.GPS); + } + if (mProximitySensor.getSelection()) { + mHardware.addSensor(Sensor.PROXIMITY_SENSOR); + } + if (mCameraFront.getSelection()) { + Camera c = new Camera(); + c.setAutofocus(true); + c.setFlash(true); + c.setLocation(CameraLocation.FRONT); + mHardware.addCamera(c); + } + if (mCameraRear.getSelection()) { + Camera c = new Camera(); + c.setAutofocus(true); + c.setFlash(true); + c.setLocation(CameraLocation.BACK); + mHardware.addCamera(c); + } + if (mKeyboard.getSelection()) { + mHardware.setKeyboard(Keyboard.QWERTY); + } else { + mHardware.setKeyboard(Keyboard.NOKEY); + } + if (mDpad.getSelection()) { + mHardware.setNav(Navigation.DPAD); + } else if (mTrackball.getSelection()) { + mHardware.setNav(Navigation.TRACKBALL); + } else { + mHardware.setNav(Navigation.NONAV); + } + long ram = Long.parseLong(mRam.getText()); + Storage.Unit unit = Storage.Unit.getEnum(mRamCombo.getText()); + mHardware.setRam(new Storage(ram, unit)); + if (mButtons.getText().equals(ButtonType.HARD.getDescription())) { + mHardware.setButtonType(ButtonType.HARD); + } else { + mHardware.setButtonType(ButtonType.SOFT); + } + + // Set the first enabled state to the default state + boolean defaultSelected = false; + if (mPortrait.getSelection()) { + State state = new State(); + state.setName("Portrait"); + state.setDescription("The device in portrait orientation"); + state.setOrientation(ScreenOrientation.PORTRAIT); + if (mHardware.getNav().equals(Navigation.NONAV) || !mPortraitNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) { + state.setKeyState(KeyboardState.SOFT); + } else { + state.setKeyState(KeyboardState.HIDDEN); + } + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mLandscape.getSelection()) { + State state = new State(); + state.setName("Landscape"); + state.setDescription("The device in landscape orientation"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) || !mLandscapeNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) { + state.setKeyState(KeyboardState.SOFT); + } else { + state.setKeyState(KeyboardState.HIDDEN); + } + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mKeyboard.getSelection()) { + if (mPortraitKeys.getSelection()) { + State state = new State(); + state.setName("Portrait with keyboard"); + state.setDescription("The device in portrait orientation with a keyboard open"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) + || !mPortraitKeysNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + state.setKeyState(KeyboardState.EXPOSED); + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mLandscapeKeys.getSelection()) { + State state = new State(); + state.setName("Landscape with keyboard"); + state.setDescription("The device in landscape orientation with a keyboard open"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) + || !mLandscapeKeysNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + state.setKeyState(KeyboardState.EXPOSED); + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + } + Device d = builder.build(); + if (mForceCreation.isEnabled() && mForceCreation.getSelection()) { + mManager.replaceUserDevice(d); + } else { + mManager.addUserDevice(d); + } + mManager.saveUserDevices(); + mCreatedDevice = d; + super.okPressed(); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java new file mode 100644 index 00000000..7ce4c743 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkuilib.internal.widgets; + +import com.android.utils.ILogger; + + +/** + * Collects all log and displays it in a message box dialog. + *

+ * This is good if only a few lines of log are expected. + * If you pass logErrorsOnly to the constructor, the message box + * will be shown only if errors were generated, which is the typical use-case. + *

+ * To use this:
+ * - Construct a new {@link IMessageBoxLogger}.
+ * - Pass the logger to the action.
+ * - Once the action is completed, call {@link #displayResult(boolean)} + * indicating whether the operation was successful or not. + * + * When logErrorsOnly is true, if the operation was not successful or errors + * were generated, this will display the message box. + */ +public interface IMessageBoxLogger extends ILogger { + + /** + * Displays the log if anything was captured. + *

+ * @param success Used only when the logger was constructed with logErrorsOnly==true. + * In this case the dialog will only be shown either if success if false or some errors + * where captured. + */ + public void displayResult(final boolean success); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java new file mode 100644 index 00000000..7f1361ec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java @@ -0,0 +1,60 @@ +/* + * 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.sdkuilib.internal.widgets; + + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; + +/** + * A label that can display 2 images depending on its enabled/disabled state. + * This acts as a button by firing the {@link SWT#Selection} listener. + */ +public class ImgDisabledButton extends ToggleButton { + public ImgDisabledButton( + Composite parent, + int style, + Image imageEnabled, + Image imageDisabled, + String tooltipEnabled, + String tooltipDisabled) { + super(parent, + style, + imageEnabled, + imageDisabled, + tooltipEnabled, + tooltipDisabled); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateImageAndTooltip(); + redraw(); + } + + @Override + public void setState(int state) { + throw new UnsupportedOperationException(); // not available for this type of button + } + + @Override + public int getState() { + return (isDisposed() || !isEnabled()) ? 1 : 0; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/PackageTypesSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/PackageTypesSelector.java new file mode 100644 index 00000000..b7569554 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/PackageTypesSelector.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package com.android.sdkuilib.internal.widgets; + +import java.util.Set; +import java.util.TreeSet; + +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.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Shell; + +import com.android.sdkuilib.internal.repository.content.PackageFilter; +import com.android.sdkuilib.internal.repository.content.PackageType; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; + +/** + * Allows the user to select the types of packages to view for installation + * @author Andrew Bowley + * + * 23-11-2017 + */ +public class PackageTypesSelector extends SwtBaseDialog { + + private static final String WINDOW_TITLE = "Select Package Types"; + private static int WINDOW_HEIGHT = 400; + private static int WINDOW_WIDTH = 200; + + public static final PackageType[] PACKAGE_TYPES = { + PackageType.tools, + PackageType.platform_tools, + PackageType.build_tools, + PackageType.platforms, + PackageType.add_ons, + PackageType.system_images, + PackageType.sources, + //PackageType.samples, + //PackageType.docs, + PackageType.extras, + //PackageType.emulator, + //PackageType.cmake, + //PackageType.lldb, + //PackageType.ndk_bundle, + //PackageType.patcher, + PackageType.generic + + }; + + private Set packageTypeSet = new TreeSet<>(); + private Set originalPackageTypeSet = new TreeSet<>(); + private SelectionAdapter selectionAdapter; + private Button buttonOK; + private Button buttonCancel; + + /** + * + */ + public PackageTypesSelector(Shell shell, Set packageTypeSet) { + super(shell, SWT.APPLICATION_MODAL, WINDOW_TITLE); + if (packageTypeSet != null) { + this.packageTypeSet.addAll(packageTypeSet); + originalPackageTypeSet.addAll(packageTypeSet); + } + selectionAdapter = getSelectionAdapter(); + } + + public Set getPackageTypeSet() { + return packageTypeSet != null ? packageTypeSet : PackageFilter.EMPTY_PACKAGE_TYPE_SET; + } + + /** + * Creates the shell for this dialog. + *

+ * Called before {@link #createContents()}. + */ + protected void createShell() { + super.createShell(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(WINDOW_WIDTH, WINDOW_HEIGHT)); + shell.setSize(WINDOW_WIDTH, WINDOW_HEIGHT); + } + + /** + * Create contents of the dialog. + */ + @Override + protected void createContents() { + Shell shell = getShell(); + GridLayoutBuilder.create(shell).columns(1); + GridDataBuilder.create(shell).fill(); + Group composite = new Group(shell, SWT.SHADOW_ETCHED_OUT); + composite.setText(WINDOW_TITLE); + GridLayoutBuilder.create(composite).margins(2); + GridDataBuilder.create(composite).hFill().hGrab(); + Display display = composite.getDisplay(); + composite.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + composite.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + boolean isPreset = !packageTypeSet.isEmpty(); + for (PackageType packageType: PACKAGE_TYPES) { + Button button = addCheck(composite, packageType, isPreset && packageTypeSet.contains(packageType)); + button.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + button.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + } + Group controls = new Group(composite, SWT.SHADOW_ETCHED_OUT); + GridDataBuilder.create(controls).vCenter().hGrab().hFill(); + GridLayoutBuilder.create(controls).columns(2); + controls.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + controls.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + buttonCancel = new Button(controls, SWT.PUSH); + buttonCancel.setText("Cancel"); + GridDataBuilder.create(buttonCancel).wHint(100).hFill().hGrab().hRight(); + buttonCancel.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + setReturnValue(false); + close(); + } + }); + buttonOK = new Button(controls, SWT.PUSH); + buttonOK.setText("OK"); + GridDataBuilder.create(buttonOK).wHint(100); + buttonOK.addSelectionListener(new SelectionAdapter() + { + @Override + public void widgetSelected(SelectionEvent e) { + setReturnValue(isDirty()); + close(); + } + }); + shell.setDefaultButton(buttonOK); + } + + @Override + protected void postCreate() { + } + + private boolean isDirty() { + if (packageTypeSet.size() != originalPackageTypeSet.size()) + return true; + return !packageTypeSet.containsAll(originalPackageTypeSet); + } + + private Button addCheck(Composite composite, PackageType packageType, boolean isSet) { + Button check = new Button(composite, SWT.CHECK); + check.setData(packageType); + check.setText(packageType.label); + //check.setToolTipText(""); + check.addSelectionListener(selectionAdapter); + if (isSet) + check.setSelection(true); + return check; + } + + private SelectionAdapter getSelectionAdapter() { + return new SelectionAdapter() { + + @Override + public void widgetSelected(SelectionEvent event) { + Button button = (Button) event.getSource(); + boolean selected = button.getSelection(); + if (selected) { + packageTypeSet.add((PackageType)button.getData()); + } else { + packageTypeSet.remove((PackageType)button.getData()); + } + } + }; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java new file mode 100644 index 00000000..528010b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java @@ -0,0 +1,138 @@ +/* + * 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.sdkuilib.internal.widgets; + + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseTrackListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; + +/** + * A label that can display 2 images depending on its internal state. + * This acts as a button by firing the {@link SWT#Selection} listener. + */ +public class ToggleButton extends CLabel { + private Image[] mImage = new Image[2]; + private String[] mTooltip = new String[2]; + private boolean mMouseIn; + private int mState = 0; + + + public ToggleButton( + Composite parent, + int style, + Image image1, + Image image2, + String tooltip1, + String tooltip2) { + super(parent, style); + mImage[0] = image1; + mImage[1] = image2; + mTooltip[0] = tooltip1; + mTooltip[1] = tooltip2; + updateImageAndTooltip(); + + Display display = parent.getDisplay(); + setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + addMouseListener(new MouseListener() { + @Override + public void mouseDown(MouseEvent e) { + // pass + } + + @Override + public void mouseUp(MouseEvent e) { + // We select on mouse-up, as it should be properly done since this is the + // only way a user can cancel a button click by moving out of the button. + if (mMouseIn && e.button == 1) { + notifyListeners(SWT.Selection, new Event()); + } + } + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (mMouseIn && e.button == 1) { + notifyListeners(SWT.DefaultSelection, new Event()); + } + } + }); + + addMouseTrackListener(new MouseTrackListener() { + @Override + public void mouseExit(MouseEvent e) { + if (mMouseIn) { + mMouseIn = false; + redraw(); + } + } + + @Override + public void mouseEnter(MouseEvent e) { + if (!mMouseIn) { + mMouseIn = true; + redraw(); + } + } + + @Override + public void mouseHover(MouseEvent e) { + // pass + } + }); + } + + @Override + public int getStyle() { + int style = super.getStyle(); + if (mMouseIn) { + style |= SWT.SHADOW_IN; + } + return style; + } + + /** + * Sets current state. + * @param state A value 0 or 1. + */ + public void setState(int state) { + assert state == 0 || state == 1; + mState = state; + updateImageAndTooltip(); + redraw(); + } + + /** + * Returns the current state + * @return Returns the current state, either 0 or 1. + */ + public int getState() { + return mState; + } + + protected void updateImageAndTooltip() { + setImage(mImage[getState()]); + setToolTipText(mTooltip[getState()]); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java new file mode 100644 index 00000000..30af250d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java @@ -0,0 +1,110 @@ +/* + * 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.sdkuilib.repository; + +import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; + +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.widgets.Shell; + +/** + * Opens an AVD Manager Window. + * + * This is the public entry point for using the window. + */ +public class AvdManagerWindow { + + /** The actual window implementation to which this class delegates. */ + private AvdManagerWindowImpl1 mWindow; + + /** + * Enum giving some indication of what is invoking this window. + * The behavior and UI will change slightly depending on the context. + *

+ * Note: if you add Android support to your specific IDE, you might want + * to specialize this context enum. + */ + public enum AvdInvocationContext { + /** + * The AVD Manager is invoked from the stand-alone 'android' tool. + * In this mode, we present an about box, a settings page. + * For SdkMan2, we also have a menu bar and link to the SDK Manager 2. + */ + STANDALONE, + + /** + * The AVD Manager is embedded as a dialog in the SDK Manager + * or in the {@link AvdSelector}. + * This is similar to the {@link #STANDALONE} mode except we don't need + * to display a menu bar at all since we don't want a menu item linking + * back to the SDK Manager and we don't need to redisplay the options + * and about which are already on the root window. + */ + DIALOG, + + /** + * The AVD Manager is invoked from an IDE. + * In this mode, we do not modify the menu bar. + * There is no about box and no settings. + */ + IDE, + } + + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkCallAgent Mediates between application and UI layer + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindow( + Shell parentShell, + SdkCallAgent sdkCallAgent, + AvdInvocationContext context) { + this(parentShell, + sdkCallAgent.getSdkContext(), + context); + } + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkContext SDK handler and repo manager + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindow( + Shell parentShell, + SdkContext sdkContext, + AvdInvocationContext context) { + mWindow = new AvdManagerWindowImpl1( + parentShell, + sdkContext, + context); + } + + /** + * Opens the window. + */ + public void open() { + mWindow.open(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java new file mode 100644 index 00000000..ffe59f8e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java @@ -0,0 +1,46 @@ +/* + * 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.sdkuilib.repository; + + +/** + * Interface for listeners on SDK modifications by the SDK Manager UI. + * This notifies both before and after the Android Sdk Handler installs pacakges + * a package. + */ +public interface ISdkChangeListener { + /** + * Invoked when the SDK Manager UI is about to start installing packages. + * This will be followed by a call to {@link #postInstallHook()}. + */ + void preInstallHook(); + + /** + * Invoked when the SDK Manager UI is done installing packages. + * Some new packages might have been installed or the user might have cancelled the operation. + * This is generally followed by a call to {@link #onSdkReload()}. + */ + void postInstallHook(); + + /** + * Invoked when the content of the SDK is being reloaded by the SDK Manager UI, + * typically after a package was installed. The SDK content might or might not + * have changed. + */ + void onSdkReload(); +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java new file mode 100644 index 00000000..0ad84440 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java @@ -0,0 +1,123 @@ +/* + * 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.sdkuilib.repository; + +import com.android.sdkuilib.internal.repository.content.PackageType; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; + +import org.eclipse.andmore.sdktool.SdkCallAgent; + +import org.eclipse.swt.widgets.Shell; + +/** + * Opens an SDK Manager Window. + * + * This is the public entry point for using the window. + */ +public class SdkUpdaterWindow { + + /** The actual window implementation to which this class delegates. */ + private final SdkUpdaterWindowImpl2 mWindow; + private final SdkCallAgent mSdkCallAgent; + + /** + * Enum giving some indication of what is invoking this window. + * The behavior and UI will change slightly depending on the context. + *

+ * Note: if you add Android support to your specific IDE, you might want + * to specialize this context enum. + */ + public enum SdkInvocationContext { + /** + * The SDK Manager is invoked from the stand-alone 'android' tool. + * In this mode, we present an about box, a settings page. + * For SdkMan2, we also have a menu bar and link to the AVD manager. + */ + STANDALONE, + + /** + * The SDK Manager is invoked from the standalone AVD Manager. + * This is similar to the standalone mode except that in this case we + * don't display a menu item linking to the AVD Manager. + */ + AVD_MANAGER, + + /** + * The SDK Manager is invoked from an IDE. + * In this mode, we do not modify the menu bar. There is no about box + * and no settings (e.g. HTTP proxy settings are inherited from Eclipse.) + */ + IDE, + + /** + * The SDK Manager is invoked from the AVD Selector. + * For SdkMan1, this means the AVD page will be displayed first. + * For SdkMan2, we won't be using this. + */ + AVD_SELECTOR + } + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkCallAgent Mediates between application and UI layer + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindow( + Shell parentShell, + SdkCallAgent sdkCallAgent, + SdkInvocationContext context) { + + this.mSdkCallAgent = sdkCallAgent; + mWindow = new SdkUpdaterWindowImpl2(parentShell, sdkCallAgent.getSdkContext(), context); + } + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + * This should be called before {@link #open()}. + */ + public void addListener(ISdkChangeListener listener) { + mWindow.addListener(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public void removeListener(ISdkChangeListener listener) { + mWindow.removeListener(listener); + } + + public void addPackageFilter(PackageType packageType) { + mWindow.addPackageFilter(packageType); + } + + /** + * Opens the window. + */ + public void open() { + try { + mWindow.open(); + } + finally { + mSdkCallAgent.completeSdkOperations(); + } + + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java new file mode 100644 index 00000000..d728eed3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java @@ -0,0 +1,601 @@ +/* + * 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.sdkuilib.ui; + +import java.io.File; +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager.RepoLoadedCallback; +import com.android.repository.api.RepoPackage; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.impl.meta.TypeDetails; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.meta.DetailsTypes.ExtraDetailsType; +import com.android.sdklib.repository.meta.DetailsTypes.PlatformDetailsType; +import com.android.sdkuilib.internal.repository.LoadPackagesRequest; +import com.android.sdkuilib.internal.repository.PackageInstallListener; +import com.android.sdkuilib.internal.repository.PackageManager; +import com.android.sdkuilib.internal.repository.Settings; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser; +import com.android.sdkuilib.internal.repository.content.PackageInstaller; +import com.android.sdkuilib.internal.repository.content.PackageType; +import com.android.sdkuilib.internal.repository.content.PackageVisitor; +import com.android.sdkuilib.internal.repository.content.PkgItem; +import com.android.sdkuilib.internal.repository.ui.SdkProgressFactory; +import com.android.sdkuilib.internal.repository.ui.SdkProgressFactory.ISdkLogWindow; +import com.android.utils.ILogger; +import com.android.utils.Pair; + +/** + * TODO - Integrate this class with SdkUpdaterWindow + * This is a private implementation of UpdateWindow for ADT, + * designed to install a very specific package. + *

+ * Example of usage: + *

+ * AdtUpdateDialog dialog = new AdtUpdateDialog(
+ *     AdtPlugin.getDisplay().getActiveShell(),
+ *     new AdtConsoleSdkLog(),
+ *     sdk.getSdkLocation());
+ *
+ * Pair result = dialog.installExtraPackage(
+ *     "android", "compatibility");  //$NON-NLS-1$ //$NON-NLS-2$
+ * or
+ * Pair result = dialog.installPlatformPackage(11);
+ * 
+ */ +public class AdtUpdateDialog extends SwtBaseDialog implements ISdkLogWindow { + + private enum TextStyle { + DEFAULT, + TITLE, + ERROR + } + + public static final int USE_MAX_REMOTE_API_LEVEL = 0; + + private static final String APP_NAME = "Android SDK Manager"; + private final SdkContext mSdkContext; + private final PackageAnalyser mPackageAnalyser; + PackageInstaller packageInstaller; + + private PackageVisitor mPackageFilter; + + private ProgressBar mProgressBar; + private Label mStatusText; + private StyledText mStyledText; + private Button mCloseButton; + + private PackageInstallListener onIdle = new PackageInstallListener(){ + + @Override + public void onPackagesInstalled(int count) { + enableClose(); + } + }; + + /** + * Creates a new {@link AdtUpdateDialog}. + * Callers will want to call {@link #installExtraPackage} or + * {@link #installPlatformPackage} after this. + * + * @param parentShell The existing parent shell. Must not be null. + * @param sdkCallAgent Mediator between application and UI layer + */ + public AdtUpdateDialog( + Shell parentShell, + SdkCallAgent sdkCallAgent) { + super(parentShell, SWT.NONE, APP_NAME); + mSdkContext = sdkCallAgent.getSdkContext(); + mPackageAnalyser = new PackageAnalyser(mSdkContext); + + } + + /** + * Displays the update dialog and triggers installation of the requested platform + * package with the specified API level. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param apiLevel The platform API level to match. + * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use + * the highest API level available on the remote repository. + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + */ + public Pair installPlatformPackage(int apiLevel) { + mPackageFilter = createPlatformFilter(apiLevel); + open(); + boolean success = packageInstaller != null; + if (success) { + success = packageInstaller.getNumInstalled() > 0; + } + File installPath = null; + if (success) { + for (PkgItem entry : packageInstaller.getRequiredPackageItems()) { + if (entry.getMetaPackage().getPackageType() == PackageType.platforms) { + installPath = new File(mSdkContext.getLocalPath(), entry.getMainPackage().getPath().replaceAll(";", "/")); + break; + } + } + } + return Pair.of(success, installPath); + } + + /** + * Displays the update dialog and triggers installation of the requested {@code extra} + * package with the specified vendor and path attributes. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param vendor The extra package vendor string to match. + * @param path The extra package path string to match. + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + * @wbp.parser.entryPoint + */ + public Pair installExtraPackage(String vendor, String path) { + mPackageFilter = createExtraFilter(vendor, path); + open(); + boolean success = packageInstaller != null; + if (success) { + success = packageInstaller.getNumInstalled() > 0; + } + File installPath = null; + if (success) { + for (PkgItem entry : packageInstaller.getRequiredPackageItems()) { + if (entry.getMetaPackage().getPackageType() == PackageType.extras) { + installPath = new File(mSdkContext.getLocalPath(), entry.getMainPackage().getPath().replaceAll(";", "/")); + break; + } + } + } + return Pair.of(success, installPath); + } + + /** + * Displays the update dialog and triggers installation of platform-tools package. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + * @wbp.parser.entryPoint + */ + public Pair installPlatformTools() { + mPackageFilter = createPlatformToolsFilter(); + open(); + boolean success = packageInstaller != null; + if (success) { + success = packageInstaller.getNumInstalled() > 0; + } + File installPath = null; + if (success) { + for (PkgItem entry : packageInstaller.getRequiredPackageItems()) { + if (entry.getMetaPackage().getPackageType() == PackageType.platform_tools) { + installPath = new File(mSdkContext.getLocalPath(), entry.getMainPackage().getPath().replaceAll(";", "/")); + break; + } + } + } + + return Pair.of(success, installPath); + } + + /** + * Displays the update dialog and triggers installation of a new SDK. This works by + * requesting a remote platform package with the specified API levels as well as + * the first tools or platform-tools packages available. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param apiLevels A set of platform API levels to match. + * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use + * the highest API level available in the repository. + * @return A boolean indicating whether the installation was successful (meaning the packages + * were either already present, or got installed or updated properly). + */ + public boolean installNewSdk(Set apiLevels) { + mPackageFilter = createNewSdkFilter(apiLevels); + open(); + boolean success = packageInstaller != null; + if (success) { + success = packageInstaller.getNumInstalled() > 0; + } + return success; + } + + @Override + protected void createContents() { + Shell shell = getShell(); + GridLayout glShell = new GridLayout(2, false); + glShell.verticalSpacing = 0; + glShell.horizontalSpacing = 0; + glShell.marginWidth = 0; + glShell.marginHeight = 0; + shell.setLayout(glShell); + + shell.setMinimumSize(new Point(600, 300)); + shell.setSize(700, 500); + GridLayoutBuilder.create(shell).columns(1); + Composite composite1 = new Composite(shell, SWT.NONE); + composite1.setLayout(new GridLayout(1, false)); + GridDataBuilder.create(composite1).fill().grab(); + + Composite composite = new Composite(composite1, SWT.NONE); + GridLayoutBuilder.create(composite).columns(2); + GridDataBuilder.create(composite).fill().grab(); + + mStyledText = new StyledText(composite, + SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + GridDataBuilder.create(mStyledText).hSpan(2).fill().grab(); + + mStatusText = new Label(composite, SWT.NONE); + GridDataBuilder.create(mStatusText).hFill().hGrab(); + + mCloseButton = new Button(composite, SWT.NONE); + mCloseButton.setText("Stop"); + mCloseButton.setToolTipText("Aborts the installation"); + mCloseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSdkContext.getProgressIndicator().cancel(); + close(); + } + }); + mProgressBar = new ProgressBar(composite1, SWT.NONE); + GridDataBuilder.create(mProgressBar).hFill().hGrab(); + } + + @Override + protected void postCreate() { + SdkProgressFactory factory = new SdkProgressFactory(mStatusText, mProgressBar, mCloseButton, this); + initializeSettings(); + if (mSdkContext.hasError()) + { + ILogger logger = (ILogger)factory; + Iterator iterator = mSdkContext.getLogMessages().iterator(); + while(iterator.hasNext()) + logger.error(null, iterator.next()); + close(); + return; + } + mSdkContext.setSdkLogger(factory); + mSdkContext.setSdkProgressIndicator(factory); + Runnable onError = new Runnable(){ + @Override + public void run() { + enableClose(); + factory.getProgressControl().setDescription("Package operation did not complete due to error or cancellation"); + }}; + RepoLoadedCallback onSuccess = new RepoLoadedCallback(){ + @Override + public void doRun(RepositoryPackages packages) { + if (!getShell().isDisposed()) + { + mSdkContext.getPackageManager().setPackages(packages); + mPackageAnalyser.loadPackages(); + packageInstaller = new PackageInstaller(mPackageAnalyser, mPackageFilter, factory); + if (packageInstaller.getRequiredPackageItems().isEmpty()) { + // No packages were selected + onError.run(); + return; + } + factory.getProgressControl().setDescription("Done loading packages."); + packageInstaller.installPackages(getShell(), mSdkContext, onIdle); + } + }}; + loadPackages(factory, onSuccess, onError); + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + syncExec(mStatusText, new Runnable() { + @Override + public void run() { + mStatusText.setText(description); + appendLine(TextStyle.TITLE, description); + } + }); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(final String log) { + syncExec(mStatusText, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, log); + } + }); + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(final String log) { + syncExec(mStatusText, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.ERROR, log); + } + }); + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(final String log) { + syncExec(mStatusText, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, " " + log); //$NON-NLS-1$ + } + }); + } + + /** + * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. + */ + private boolean initializeSettings() { + Settings settings = new Settings(); + if (settings.initialize(mSdkContext.getSdkLog())) + { + mSdkContext.setSettings(settings); + return true; + } + return false; + } + + public static PackageVisitor createExtraFilter( + final String vendor, + final String path) { + return new PackageVisitor() { + String mVendor = vendor; + String mPath = path; + + @Override + public boolean accept(PkgItem pkg) { + if (pkg.getMetaPackage().getPackageType() == PackageType.extras) { + TypeDetails details = pkg.getMainPackage().getTypeDetails(); + if (details instanceof ExtraDetailsType) { + ExtraDetailsType extraDetailsType = (ExtraDetailsType)details; + if (extraDetailsType.getVendor().getId().equals(mVendor)) { + // Check actual extra field + if (pkg.getMainPackage().getPath().equals(mPath)) { + return true; + } + } + } + } + return false; + } + }; + } + + private PackageVisitor createPlatformToolsFilter() { + return new PackageVisitor() { + @Override + public boolean accept(PkgItem pkg) { + return pkg.getMetaPackage().getPackageType() == PackageType.platform_tools; + } + }; + } + + public static PackageVisitor createPlatformFilter(final int apiLevel) { + return new PackageVisitor() { + int mApiLevel = apiLevel; + boolean mFindMaxApi = apiLevel == USE_MAX_REMOTE_API_LEVEL; + + @Override + public boolean accept(PkgItem pkg) { + if (pkg.getMetaPackage().getPackageType() == PackageType.platforms) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if (details instanceof PlatformDetailsType) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + return !androidVersion.isPreview() && androidVersion.getApiLevel() == mApiLevel; + } + } + return false; + } + + @Override + public boolean visit(PkgItem pkg) { + // Try to find the max API in all remote packages + if (mFindMaxApi && (pkg.getMetaPackage().getPackageType() == PackageType.platforms)) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType) && (remotePackage instanceof RemotePackage)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int api = androidVersion.getApiLevel(); + if (api > mApiLevel) { + mApiLevel = api; + } + } + } + } + return true; + } + }; + } + + public static PackageVisitor createNewSdkFilter(final Set apiLevels) { + return new PackageVisitor() { + int mMaxApiLevel; + boolean mFindMaxApi = apiLevels.contains(USE_MAX_REMOTE_API_LEVEL); + boolean mNeedTools = true; + boolean mNeedPlatformTools = true; + + @Override + public boolean accept(PkgItem pkg) { + RepoPackage repoPackage = pkg.getMainPackage(); + if (repoPackage instanceof RemotePackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int level = androidVersion.getApiLevel(); + if ((mFindMaxApi && level == mMaxApiLevel) || + (level > 0 && apiLevels.contains(level))) { + return true; + } + } + } else if (mNeedTools && (pkg.getMetaPackage().getPackageType() == PackageType.tools)) { + // We want a tool package. There should be only one, + // but in case of error just take the first one. + mNeedTools = false; + return true; + } else if (mNeedPlatformTools && (pkg.getMetaPackage().getPackageType() == PackageType.platform_tools)) { + // We want a platform-tool package. There should be only one, + // but in case of error just take the first one. + mNeedPlatformTools = false; + return true; + } + } + return false; + } + + @Override + public boolean visit(PkgItem pkg) { + // Try to find the max API in all remote packages + if (mFindMaxApi && (pkg.getMetaPackage().getPackageType() == PackageType.platforms)) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType) && (remotePackage instanceof RemotePackage)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int api = androidVersion.getApiLevel(); + if (api > mMaxApiLevel) { + mMaxApiLevel = api; + } + } + } + } + return true; + } + }; + } + + private void loadPackages(ProgressRunner progressRunner, RepoLoadedCallback onSuccess, Runnable onError) { + final PackageManager packageManager = mSdkContext.getPackageManager(); + LoadPackagesRequest loadPackagesRequest = new LoadPackagesRequest(progressRunner); + //loadPackagesRequest.setOnLocalComplete(Collections.singletonList(onLocalComplete)); + loadPackagesRequest.setOnSuccess(Collections.singletonList(onSuccess)); + loadPackagesRequest.setOnError(Collections.singletonList(onError)); + packageManager.requestRepositoryPackages(loadPackagesRequest); + } + + + private void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(runnable); + } + } + + private void appendLine(TextStyle style, String text) { + if (!text.endsWith("\n")) { //$NON-NLS-1$ + text += '\n'; + } + + int start = mStyledText.getCharCount(); + + if (style == TextStyle.DEFAULT) { + mStyledText.append(text); + + } else { + mStyledText.append(text); + + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = text.length(); + sr.fontStyle = SWT.BOLD; + if (style == TextStyle.ERROR) { + sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED); + } + sr.underline = false; + mStyledText.setStyleRange(sr); + } + + // Scroll caret if it was already at the end before we added new text. + // Ideally we would scroll if the scrollbar is at the bottom but we don't + // have direct access to the scrollbar without overriding the SWT impl. + if (mStyledText.getCaretOffset() >= start) { + mStyledText.setSelection(mStyledText.getCharCount()); + } + } + + @Override + public void show() { + } + + private void enableClose() { + syncExec(mCloseButton, new Runnable() { + @Override + public void run() { + mCloseButton.setEnabled(true); + mCloseButton.setText("OK"); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java new file mode 100644 index 00000000..c077369f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java @@ -0,0 +1,195 @@ +/* + * 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.sdkuilib.ui; + +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.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Dialog which collects from the user his/her login and password. + */ +public class AuthenticationDialog extends GridDialog { + private Text mTxtLogin; + private Text mTxtPassword; + private Text mTxtWorkstation; + private Text mTxtDomain; + + private String mTitle; + private String mMessage; + + private static String sLogin = ""; + private static String sPassword = ""; + private static String sWorkstation = ""; + private static String sDomain = ""; + + /** + * Constructor which retrieves the parent {@link Shell} and the message to + * be displayed in this dialog. + * + * @param parentShell Parent Shell + * @param title Title of the window. + * @param message Message the be displayed in this dialog. + */ + public AuthenticationDialog(Shell parentShell, String title, String message) { + super(parentShell, 1, false); + // assign fields + mTitle = title; + mMessage = message; + } + + @Override + public void createDialogContent(Composite parent) { + // Configure Dialog + getShell().setText(mTitle); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + parent.setLayoutData(data); + + // Upper Composite + Composite upperComposite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.verticalSpacing = 10; + upperComposite.setLayout(layout); + data = new GridData(SWT.FILL, SWT.CENTER, true, true); + upperComposite.setLayoutData(data); + + // add message label + Label lblMessage = new Label(upperComposite, SWT.WRAP); + lblMessage.setText(mMessage); + data = new GridData(SWT.FILL, SWT.CENTER, true, true, 2, 1); + data.widthHint = 500; + lblMessage.setLayoutData(data); + + // add user name label and text field + Label lblUserName = new Label(upperComposite, SWT.NONE); + lblUserName.setText("Login:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblUserName.setLayoutData(data); + + mTxtLogin = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtLogin.setLayoutData(data); + mTxtLogin.setFocus(); + mTxtLogin.setText(sLogin); + mTxtLogin.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sLogin = mTxtLogin.getText().trim(); + } + }); + + // add password label and text field + Label lblPassword = new Label(upperComposite, SWT.NONE); + lblPassword.setText("Password:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblPassword.setLayoutData(data); + + mTxtPassword = new Text(upperComposite, SWT.SINGLE | SWT.PASSWORD | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtPassword.setLayoutData(data); + mTxtPassword.setText(sPassword); + mTxtPassword.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sPassword = mTxtPassword.getText(); + } + }); + + // add a label indicating that the following two fields are optional + Label lblInfo = new Label(upperComposite, SWT.NONE); + lblInfo.setText("Provide the following info if your proxy uses NTLM authentication. Leave blank otherwise."); + data = new GridData(); + data.horizontalSpan = 2; + lblInfo.setLayoutData(data); + + // add workstation label and text field + Label lblWorkstation = new Label(upperComposite, SWT.NONE); + lblWorkstation.setText("Workstation:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblWorkstation.setLayoutData(data); + + mTxtWorkstation = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtWorkstation.setLayoutData(data); + mTxtWorkstation.setText(sWorkstation); + mTxtWorkstation.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sWorkstation = mTxtWorkstation.getText().trim(); + } + }); + + // add domain label and text field + Label lblDomain = new Label(upperComposite, SWT.NONE); + lblDomain.setText("Domain:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblDomain.setLayoutData(data); + + mTxtDomain = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtDomain.setLayoutData(data); + mTxtDomain.setText(sDomain); + mTxtDomain.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sDomain = mTxtDomain.getText().trim(); + } + }); + } + + /** + * Retrieves the Login field information + * + * @return Login field value or empty String. Return value is never null + */ + public String getLogin() { + return sLogin; + } + + /** + * Retrieves the Password field information + * + * @return Password field value or empty String. Return value is never null + */ + public String getPassword() { + return sPassword; + } + + /** + * Retrieves the workstation field information + * + * @return Workstation field value or empty String. Return value is never null + */ + public String getWorkstation() { + return sWorkstation; + } + + /** + * Retrieves the domain field information + * + * @return Domain field value or empty String. Return value is never null + */ + public String getDomain() { + return sDomain; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java new file mode 100644 index 00000000..573154ca --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.sdkuilib.ui; + +import com.android.sdkuilib.internal.widgets.AvdSelector; + +/** + * The display mode of the AVD Selector. + */ +public enum AvdDisplayMode { + /** + * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs + */ + MANAGER, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "check" selection mode, checkboxes are displayed on each line + * and {@link AvdSelector#getSelected()} returns the line that is checked + * even if it is not the currently selected line. Only one line can + * be checked at once. + */ + SIMPLE_CHECK, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "select" selection mode, there are no checkboxes and + * {@link AvdSelector#getSelected()} returns the line currently selected. + * Only one line can be selected at once. + */ + SIMPLE_SELECTION, + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java new file mode 100644 index 00000000..a98fd444 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.sdkuilib.ui; + +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; +import org.eclipse.andmore.sdktool.Utilities.Compatibility; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.TableItem; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.repository.avd.SystemImageInfo; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; + +/** + * A control to select an Android Virtual Device (AVD) + * @author Andrew Bowley + * + */ +public class AvdSelectorWindow { + + private final AvdSelector avdSelector; + private final SdkContext sdkContext; + private final SdkTargets sdkTargets; + + public AvdSelectorWindow(Composite parent, SdkCallAgent sdkCallAgent) { + this.sdkContext = sdkCallAgent.getSdkContext(); + sdkTargets = new SdkTargets(sdkContext); + avdSelector = new AvdSelector(parent, sdkContext, (IAvdFilter)null, AvdDisplayMode.SIMPLE_CHECK); + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public void setSelection(AvdInfo avd) { + avdSelector.setSelection(avd); + } + + /** + * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will + * return the {@link AvdInfo} that is checked instead of the list selection. + * + * @return The currently selected item or null. + */ + public AvdInfo getSelected() { + AvdAgent avdAgent = avdSelector.getSelected(); + return avdAgent != null ? avdAgent.getAvd() : null; + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + avdSelector.setTableHeightHint(heightHint); + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + *

+ * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to + * display the details of the selected AVD.
+ * To disable it (when you provide your own double click action), set + * {@link SelectionEvent#doit} to false in + * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + avdSelector.setSelectionListener(selectionListener); + } + + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + avdSelector.setEnabled(enabled); + } + + /** + * Sets a new AVD manager and updates AVD filter parameters + * This also refreshes the display + * @param manager the AVD manager. + */ + public void setManager(AvdManager manager, IAndroidTarget target, AndroidVersion minApiVersion) { + avdSelector.setManager(manager); + avdSelector.refresh(false); + avdSelector.setFilter(getCompatibilityFilter(target, minApiVersion)); + } + + private IAvdFilter getCompatibilityFilter(IAndroidTarget target, AndroidVersion minApiVersion) { + return new IAvdFilter() { + + @Override + public void prepare() { + } + + @Override + public void cleanup() { + } + + @Override + public boolean accept(AvdAgent avdAgent) { + AvdInfo info = avdAgent.getAvd(); + Compatibility c = + Utilities.canRun(info, getAndroidTargetFor(info), target, minApiVersion); + return (c == Compatibility.NO) ? false : true; + } + }; + } + + private IAndroidTarget getAndroidTargetFor(AvdInfo info) { + SystemImageInfo systemImageInfo = new SystemImageInfo(info); + if (systemImageInfo.hasSystemImage()) + return sdkTargets.getTargetForSysImage(systemImageInfo.getSystemImage()); + return avdSelector.getSdkTargets().getTargetForAndroidVersion(info.getAndroidVersion()); + } +} diff --git a/android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java similarity index 86% rename from android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java index bd445ad1..f524cafd 100644 --- a/android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java @@ -1,824 +1,824 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.eclipse.andmore.internal.launch; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; -import com.android.ddmlib.Client; -import com.android.ddmlib.IDevice; -import com.android.ddmlib.IDevice.DeviceState; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.TableHelper; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdkuilib.internal.widgets.AvdSelector; -import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode; -import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.internal.editors.IconFactory; -import org.eclipse.andmore.internal.sdk.AdtConsoleSdkLog; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.jface.dialogs.Dialog; -import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.jface.viewers.ILabelProviderListener; -import org.eclipse.jface.viewers.IStructuredContentProvider; -import org.eclipse.jface.viewers.ITableLabelProvider; -import org.eclipse.jface.viewers.StructuredSelection; -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.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.Control; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.swt.widgets.Table; - -import java.util.ArrayList; -import java.util.List; - -/** - * A dialog that lets the user choose a device to deploy an application. - * The user can either choose an exiting running device (including running emulators) - * or start a new emulator using an Android Virtual Device configuration that matches - * the current project. - */ -public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { - - private final static int ICON_WIDTH = 16; - - private Table mDeviceTable; - private TableViewer mViewer; - private AvdSelector mPreferredAvdSelector; - - private Image mDeviceImage; - private Image mEmulatorImage; - private Image mMatchImage; - private Image mNoMatchImage; - private Image mWarningImage; - - private final DeviceChooserResponse mResponse; - private final String mPackageName; - private final IAndroidTarget mProjectTarget; - private final AndroidVersion mMinApiVersion; - private final Sdk mSdk; - - private Button mDeviceRadioButton; - private Button mUseDeviceForFutureLaunchesCheckbox; - private boolean mUseDeviceForFutureLaunches; - - private boolean mDisableAvdSelectionChange = false; - - /** - * Basic Content Provider for a table full of {@link IDevice} objects. The input is - * a {@link AndroidDebugBridge}. - */ - private class ContentProvider implements IStructuredContentProvider { - @Override - public Object[] getElements(Object inputElement) { - if (inputElement instanceof AndroidDebugBridge) { - return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices()); - } - - return new Object[0]; - } - - private Object[] findCompatibleDevices(IDevice[] devices) { - if (devices == null) { - return null; - } - - List compatibleDevices = new ArrayList(devices.length); - for (IDevice device : devices) { - AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); - if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) { - compatibleDevices.add(device); - } - } - - return compatibleDevices.toArray(); - } - - @Override - public void dispose() { - // pass - } - - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // pass - } - } - - /** - * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. - * It provides labels and images for {@link IDevice} objects. - */ - private class LabelProvider implements ITableLabelProvider { - - @Override - public Image getColumnImage(Object element, int columnIndex) { - if (element instanceof IDevice) { - IDevice device = (IDevice)element; - switch (columnIndex) { - case 0: - return device.isEmulator() ? mEmulatorImage : mDeviceImage; - - case 2: - // check for compatibility. - if (device.isEmulator() == false) { // physical device - // get the version of the device - AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); - if (deviceVersion == null) { - return mWarningImage; - } else { - if (!deviceVersion.canRun(mMinApiVersion)) { - return mNoMatchImage; - } - - // if the project is compiling against an add-on, - // the optional API may be missing from the device. - return mProjectTarget.isPlatform() ? - mMatchImage : mWarningImage; - } - } else { - // get the AvdInfo - AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), - true /*validAvdOnly*/); - AvdCompatibility.Compatibility c = - AvdCompatibility.canRun(info, mProjectTarget, - mMinApiVersion); - switch (c) { - case YES: - return mMatchImage; - case NO: - return mNoMatchImage; - case UNKNOWN: - return mWarningImage; - } - } - } - } - - return null; - } - - @Override - public String getColumnText(Object element, int columnIndex) { - if (element instanceof IDevice) { - IDevice device = (IDevice)element; - switch (columnIndex) { - case 0: - return device.getName(); - case 1: - if (device.isEmulator()) { - return device.getAvdName(); - } else { - return "N/A"; // devices don't have AVD names. - } - case 2: - if (device.isEmulator()) { - AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), - true /*validAvdOnly*/); - if (info == null) { - return "?"; - } - return info.getTarget().getFullName(); - } else { - String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); - if (deviceBuild == null) { - return "unknown"; - } - return deviceBuild; - } - case 3: - String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); - if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ - return "Yes"; - } else { - return ""; - } - case 4: - return getStateString(device); - } - } - - 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 - } - } - - public static class DeviceChooserResponse { - private AvdInfo mAvdToLaunch; - private IDevice mDeviceToUse; - private boolean mUseDeviceForFutureLaunches; - - public void setDeviceToUse(IDevice d) { - mDeviceToUse = d; - mAvdToLaunch = null; - } - - public void setAvdToLaunch(AvdInfo avd) { - mAvdToLaunch = avd; - mDeviceToUse = null; - } - - public IDevice getDeviceToUse() { - return mDeviceToUse; - } - - public AvdInfo getAvdToLaunch() { - return mAvdToLaunch; - } - - public void setUseDeviceForFutureLaunches(boolean en) { - mUseDeviceForFutureLaunches = en; - } - - public boolean useDeviceForFutureLaunches() { - return mUseDeviceForFutureLaunches; - } - } - - public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName, - IAndroidTarget projectTarget, AndroidVersion minApiVersion, - boolean useDeviceForFutureLaunches) { - super(parent); - - mResponse = response; - mPackageName = packageName; - mProjectTarget = projectTarget; - mMinApiVersion = minApiVersion; - mSdk = Sdk.getCurrent(); - mUseDeviceForFutureLaunches = useDeviceForFutureLaunches; - - AndroidDebugBridge.addDeviceChangeListener(this); - loadImages(); - } - - private void cleanup() { - // done listening. - AndroidDebugBridge.removeDeviceChangeListener(this); - } - - @Override - protected void okPressed() { - cleanup(); - super.okPressed(); - } - - @Override - protected void cancelPressed() { - cleanup(); - super.cancelPressed(); - } - - @Override - protected Control createContents(Composite parent) { - Control content = super.createContents(parent); - - // this must be called after createContents() has happened so that the - // ok button has been created (it's created after the call to createDialogArea) - updateDefaultSelection(); - - return content; - } - - /** - * Create the button bar: We override the Dialog implementation of this method - * so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons. - */ - @Override - protected Control createButtonBar(Composite parent) { - Composite composite = new Composite(parent, SWT.NONE); - - GridLayout layout = new GridLayout(1, false); - layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); - composite.setLayout(layout); - composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK); - mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches); - mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); - mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches"); - mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - mUseDeviceForFutureLaunches = - mUseDeviceForFutureLaunchesCheckbox.getSelection(); - mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); - } - }); - mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); - createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); - - return composite; - } - - @Override - protected Control createDialogArea(Composite parent) { - // set dialog title - getShell().setText("Android Device Chooser"); - - Composite top = new Composite(parent, SWT.NONE); - top.setLayout(new GridLayout(1, true)); - - String msg; - if (mProjectTarget.isPlatform()) { - msg = String.format("Select a device with min API level %s.", - mMinApiVersion.getApiString()); - } else { - msg = String.format("Select a device compatible with target %s.", - mProjectTarget.getFullName()); - } - Label label = new Label(top, SWT.NONE); - label.setText(msg); - - mDeviceRadioButton = new Button(top, SWT.RADIO); - mDeviceRadioButton.setText("Choose a running Android device"); - mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - boolean deviceMode = mDeviceRadioButton.getSelection(); - - mDeviceTable.setEnabled(deviceMode); - mPreferredAvdSelector.setEnabled(!deviceMode); - - if (deviceMode) { - handleDeviceSelection(); - } else { - mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); - } - - enableOkButton(); - } - }); - mDeviceRadioButton.setSelection(true); - - - // offset the selector from the radio button - Composite offsetComp = new Composite(top, SWT.NONE); - offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - GridLayout layout = new GridLayout(1, false); - layout.marginRight = layout.marginHeight = 0; - layout.marginLeft = 30; - offsetComp.setLayout(layout); - - mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); - GridData gd; - mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); - gd.heightHint = 100; - - mDeviceTable.setHeaderVisible(true); - mDeviceTable.setLinesVisible(true); - - TableHelper.createTableColumn(mDeviceTable, "Serial Number", - SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "AVD Name", - SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "Target", - SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "Debug", - SWT.LEFT, "Debug", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "State", - SWT.LEFT, "bootloader", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - // create the viewer for it - mViewer = new TableViewer(mDeviceTable); - mViewer.setContentProvider(new ContentProvider()); - mViewer.setLabelProvider(new LabelProvider()); - mViewer.setInput(AndroidDebugBridge.getBridge()); - - mDeviceTable.addSelectionListener(new SelectionAdapter() { - /** - * Handles single-click selection on the device selector. - * {@inheritDoc} - */ - @Override - public void widgetSelected(SelectionEvent e) { - handleDeviceSelection(); - } - - /** - * Handles double-click selection on the device selector. - * Note that the single-click handler will probably already have been called. - * {@inheritDoc} - */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - handleDeviceSelection(); - if (isOkButtonEnabled()) { - okPressed(); - } - } - }); - - Button radio2 = new Button(top, SWT.RADIO); - radio2.setText("Launch a new Android Virtual Device"); - - // offset the selector from the radio button - offsetComp = new Composite(top, SWT.NONE); - offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - layout = new GridLayout(1, false); - layout.marginRight = layout.marginHeight = 0; - layout.marginLeft = 30; - offsetComp.setLayout(layout); - - mPreferredAvdSelector = new AvdSelector(offsetComp, - mSdk.getSdkOsLocation(), - mSdk.getAvdManager(), - new NonRunningAvdFilter(), - DisplayMode.SIMPLE_SELECTION, - new AdtConsoleSdkLog()); - mPreferredAvdSelector.setTableHeightHint(100); - mPreferredAvdSelector.setEnabled(false); - mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { - /** - * Handles single-click selection on the AVD selector. - * {@inheritDoc} - */ - @Override - public void widgetSelected(SelectionEvent e) { - if (mDisableAvdSelectionChange == false) { - mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); - enableOkButton(); - } - } - - /** - * Handles double-click selection on the AVD selector. - * - * Note that the single-click handler will probably already have been called - * but the selected item can have changed in between. - * - * {@inheritDoc} - */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - widgetSelected(e); - if (isOkButtonEnabled()) { - okPressed(); - } - } - }); - - return top; - } - - private void loadImages() { - ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader(); - Display display = DdmsPlugin.getDisplay(); - IconFactory factory = IconFactory.getInstance(); - - if (mDeviceImage == null) { - mDeviceImage = ddmUiLibLoader.loadImage(display, - "device.png", //$NON-NLS-1$ - ICON_WIDTH, ICON_WIDTH, - display.getSystemColor(SWT.COLOR_RED)); - } - if (mEmulatorImage == null) { - mEmulatorImage = ddmUiLibLoader.loadImage(display, - "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_BLUE)); - } - - if (mMatchImage == null) { - mMatchImage = factory.getIcon("match", //$NON-NLS-1$ - IconFactory.COLOR_GREEN, - IconFactory.SHAPE_DEFAULT); - } - - if (mNoMatchImage == null) { - mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$ - IconFactory.COLOR_RED, - IconFactory.SHAPE_DEFAULT); - } - - if (mWarningImage == null) { - mWarningImage = factory.getIcon("warning", //$NON-NLS-1$ - SWT.COLOR_YELLOW, - IconFactory.SHAPE_DEFAULT); - } - - } - - /** - * 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 "??"; - } - - /** - * Sent when the a device is connected to the {@link AndroidDebugBridge}. - *

- * This is sent from a non UI thread. - * @param device the new device. - * - * @see IDeviceChangeListener#deviceConnected(IDevice) - */ - @Override - public void deviceConnected(IDevice device) { - final DeviceChooserDialog dialog = this; - exec(new Runnable() { - @Override - public void run() { - if (mDeviceTable.isDisposed() == false) { - // refresh all - mViewer.refresh(); - - // update the selection - updateDefaultSelection(); - - // update the display of AvdInfo (since it's filtered to only display - // non running AVD.) - refillAvdList(false /*reloadAvds*/); - } else { - // table is disposed, we need to do something. - // lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(dialog); - } - - } - }); - } - - /** - * Sent when the a device is connected to the {@link AndroidDebugBridge}. - *

- * This is sent from a non UI thread. - * @param device the new device. - * - * @see IDeviceChangeListener#deviceDisconnected(IDevice) - */ - @Override - public void deviceDisconnected(IDevice device) { - deviceConnected(device); - } - - /** - * Sent when a device data changed, or when clients are started/terminated on the device. - *

- * This is sent from a non UI thread. - * @param device the device that was updated. - * @param changeMask the mask indicating what changed. - * - * @see IDeviceChangeListener#deviceChanged(IDevice, int) - */ - @Override - public void deviceChanged(final IDevice device, int changeMask) { - if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) { - final DeviceChooserDialog dialog = this; - exec(new Runnable() { - @Override - public void run() { - if (mDeviceTable.isDisposed() == false) { - // refresh the device - mViewer.refresh(device); - - // update the defaultSelection. - updateDefaultSelection(); - - // update the display of AvdInfo (since it's filtered to only display - // non running AVD). This is done on deviceChanged because the avd name - // of a (emulator) device may be updated as the emulator boots. - - refillAvdList(false /*reloadAvds*/); - - // if the changed device is the current selection, - // we update the OK button based on its state. - if (device == mResponse.getDeviceToUse()) { - enableOkButton(); - } - - } else { - // table is disposed, we need to do something. - // lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(dialog); - } - } - }); - } - } - - /** - * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). - */ - private boolean isDeviceMode() { - return mDeviceRadioButton.getSelection(); - } - - /** - * Enables or disables the OK button of the dialog based on various selections in the dialog. - */ - private void enableOkButton() { - Button okButton = getButton(IDialogConstants.OK_ID); - - if (isDeviceMode()) { - okButton.setEnabled(mResponse.getDeviceToUse() != null && - mResponse.getDeviceToUse().isOnline()); - } else { - okButton.setEnabled(mResponse.getAvdToLaunch() != null); - } - } - - /** - * Returns true if the ok button is enabled. - */ - private boolean isOkButtonEnabled() { - Button okButton = getButton(IDialogConstants.OK_ID); - return okButton.isEnabled(); - } - - /** - * Executes the {@link Runnable} in the UI thread. - * @param runnable the runnable to execute. - */ - private void exec(Runnable runnable) { - try { - Display display = mDeviceTable.getDisplay(); - display.asyncExec(runnable); - } catch (SWTException e) { - // tree is disposed, we need to do something. lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(this); - } - } - - private void handleDeviceSelection() { - int count = mDeviceTable.getSelectionCount(); - if (count != 1) { - handleSelection(null); - } else { - int index = mDeviceTable.getSelectionIndex(); - Object data = mViewer.getElementAt(index); - if (data instanceof IDevice) { - handleSelection((IDevice)data); - } else { - handleSelection(null); - } - } - } - - private void handleSelection(IDevice device) { - mResponse.setDeviceToUse(device); - enableOkButton(); - } - - /** - * Look for a default device to select. This is done by looking for the running - * clients on each device and finding one similar to the one being launched. - *

- * This is done every time the device list changed unless there is a already selection. - */ - private void updateDefaultSelection() { - if (mDeviceTable.getSelectionCount() == 0) { - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - - IDevice[] devices = bridge.getDevices(); - - for (IDevice device : devices) { - Client[] clients = device.getClients(); - - for (Client client : clients) { - - if (mPackageName.equals(client.getClientData().getClientDescription())) { - // found a match! Select it. - mViewer.setSelection(new StructuredSelection(device)); - handleSelection(device); - - // and we're done. - return; - } - } - } - } - - handleDeviceSelection(); - } - - private final class NonRunningAvdFilter implements IAvdFilter { - - private IDevice[] mDevices; - - @Override - public void prepare() { - mDevices = AndroidDebugBridge.getBridge().getDevices(); - } - - @Override - public boolean accept(AvdInfo avd) { - if (mDevices != null) { - for (IDevice d : mDevices) { - // do not accept running avd's - if (avd.getName().equals(d.getAvdName())) { - return false; - } - - // only accept avd's that can actually run the project - AvdCompatibility.Compatibility c = - AvdCompatibility.canRun(avd, mProjectTarget, mMinApiVersion); - return (c == AvdCompatibility.Compatibility.NO) ? false : true; - } - } - - return true; - } - - @Override - public void cleanup() { - mDevices = null; - } - } - - /** - * Refills the AVD list keeping the current selection. - */ - private void refillAvdList(boolean reloadAvds) { - // save the current selection - AvdInfo selected = mPreferredAvdSelector.getSelected(); - - // disable selection change. - mDisableAvdSelectionChange = true; - - // refresh the list - mPreferredAvdSelector.refresh(false); - - // attempt to reselect the proper avd if needed - if (selected != null) { - if (mPreferredAvdSelector.setSelection(selected) == false) { - // looks like the selection is lost. this can happen if an emulator - // running the AVD that was selected was launched from outside of Eclipse). - mResponse.setAvdToLaunch(null); - enableOkButton(); - } - } - - // enable the selection change - mDisableAvdSelectionChange = false; - } -} - +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.sdkuilib.ui; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmuilib.TableHelper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; + +import org.eclipse.andmore.base.resources.IEditorIconFactory; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.StructuredSelection; +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.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.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; + +import java.util.ArrayList; +import java.util.List; + +/** + * A dialog that lets the user choose a device to deploy an application. + * The user can either choose an exiting running device (including running emulators) + * or start a new emulator using an Android Virtual Device configuration that matches + * the current project. + */ +public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { + + private Table mDeviceTable; + private TableViewer mViewer; + private AvdSelector mPreferredAvdSelector; + + private Image mDeviceImage; + private Image mEmulatorImage; + private Image mMatchImage; + private Image mNoMatchImage; + private Image mWarningImage; + + private final DeviceChooserResponse mResponse; + private final String mPackageName; + private final IAndroidTarget mProjectTarget; + private final AndroidVersion mMinApiVersion; + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; + private final IEditorIconFactory mIconFactory; + + private Button mDeviceRadioButton; + private Button mUseDeviceForFutureLaunchesCheckbox; + private boolean mUseDeviceForFutureLaunches; + + private boolean mDisableAvdSelectionChange = false; + + /** + * Basic Content Provider for a table full of {@link IDevice} objects. The input is + * a {@link AndroidDebugBridge}. + */ + private class ContentProvider implements IStructuredContentProvider { + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices()); + } + + return new Object[0]; + } + + private Object[] findCompatibleDevices(IDevice[] devices) { + if (devices == null) { + return null; + } + + List compatibleDevices = new ArrayList(devices.length); + for (IDevice device : devices) { + AndroidVersion deviceVersion = Utilities.getDeviceVersion(device); + if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) { + compatibleDevices.add(device); + } + } + + return compatibleDevices.toArray(); + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. + * It provides labels and images for {@link IDevice} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case 0: + return device.isEmulator() ? mEmulatorImage : mDeviceImage; + + case 2: + // check for compatibility. + if (device.isEmulator() == false) { // physical device + // get the version of the device + AndroidVersion deviceVersion = Utilities.getDeviceVersion(device); + if (deviceVersion == null) { + return mWarningImage; + } else { + if (!deviceVersion.canRun(mMinApiVersion)) { + return mNoMatchImage; + } + + // if the project is compiling against an add-on, + // the optional API may be missing from the device. + return mProjectTarget.isPlatform() ? + mMatchImage : mWarningImage; + } + } else { + // get the AvdInfo + AvdInfo info = mSdkContext.getAvdManager().getAvd(device.getAvdName(), + true /*validAvdOnly*/); + if (info == null) + return mWarningImage; + IAndroidTarget avdTarget = getAndroidTargetFor(info); + Utilities.Compatibility c = + Utilities.canRun(info, mProjectTarget, avdTarget, mMinApiVersion); + switch (c) { + case YES: + return mMatchImage; + case NO: + return mNoMatchImage; + case UNKNOWN: + return mWarningImage; + } + } + } + } + + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case 0: + return device.getName(); + case 1: + if (device.isEmulator()) { + return device.getAvdName(); + } else { + return "N/A"; // devices don't have AVD names. + } + case 2: + if (device.isEmulator()) { + AvdInfo info = mSdkContext.getAvdManager().getAvd(device.getAvdName(), + true /*validAvdOnly*/); + if (info == null) { + return "?"; + } + return getAndroidTargetFor(info).getFullName(); + } else { + String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); + if (deviceBuild == null) { + return "unknown"; + } + return deviceBuild; + } + case 3: + String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return "Yes"; + } else { + return ""; + } + case 4: + return getStateString(device); + } + } + + 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 + } + } + + public static class DeviceChooserResponse { + private AvdInfo mAvdToLaunch; + private IDevice mDeviceToUse; + private boolean mUseDeviceForFutureLaunches; + + public void setDeviceToUse(IDevice d) { + mDeviceToUse = d; + mAvdToLaunch = null; + } + + public void setAvdToLaunch(AvdInfo avd) { + mAvdToLaunch = avd; + mDeviceToUse = null; + } + + public IDevice getDeviceToUse() { + return mDeviceToUse; + } + + public AvdInfo getAvdToLaunch() { + return mAvdToLaunch; + } + + public void setUseDeviceForFutureLaunches(boolean en) { + mUseDeviceForFutureLaunches = en; + } + + public boolean useDeviceForFutureLaunches() { + return mUseDeviceForFutureLaunches; + } + } + + public DeviceChooserDialog(Shell parent, SdkCallAgent sdkCallAgent, DeviceChooserResponse response, String packageName, + IAndroidTarget projectTarget, AndroidVersion minApiVersion, + boolean useDeviceForFutureLaunches) { + super(parent); + + mSdkContext = sdkCallAgent.getSdkContext(); + mSdkTargets = new SdkTargets(mSdkContext); + mIconFactory = sdkCallAgent.getEditorIconFactory(); + mResponse = response; + mPackageName = packageName; + mProjectTarget = projectTarget; + mMinApiVersion = minApiVersion; + mUseDeviceForFutureLaunches = useDeviceForFutureLaunches; + + AndroidDebugBridge.addDeviceChangeListener(this); + loadImages(parent.getDisplay()); + } + + private void cleanup() { + // done listening. + AndroidDebugBridge.removeDeviceChangeListener(this); + } + + @Override + protected void okPressed() { + cleanup(); + super.okPressed(); + } + + @Override + protected void cancelPressed() { + cleanup(); + super.cancelPressed(); + } + + @Override + protected Control createContents(Composite parent) { + Control content = super.createContents(parent); + + // this must be called after createContents() has happened so that the + // ok button has been created (it's created after the call to createDialogArea) + updateDefaultSelection(); + + return content; + } + + /** + * Create the button bar: We override the Dialog implementation of this method + * so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons. + */ + @Override + protected Control createButtonBar(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + + GridLayout layout = new GridLayout(1, false); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + composite.setLayout(layout); + composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK); + mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches); + mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); + mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches"); + mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mUseDeviceForFutureLaunches = + mUseDeviceForFutureLaunchesCheckbox.getSelection(); + mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); + } + }); + mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + + return composite; + } + + @Override + protected Control createDialogArea(Composite parent) { + // set dialog title + getShell().setText("Android Device Chooser"); + + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, true)); + + String msg; + if (mProjectTarget.isPlatform()) { + msg = String.format("Select a device with min API level %s.", + mMinApiVersion.getApiString()); + } else { + msg = String.format("Select a device compatible with target %s.", + mProjectTarget.getFullName()); + } + Label label = new Label(top, SWT.NONE); + label.setText(msg); + + mDeviceRadioButton = new Button(top, SWT.RADIO); + mDeviceRadioButton.setText("Choose a running Android device"); + mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean deviceMode = mDeviceRadioButton.getSelection(); + + mDeviceTable.setEnabled(deviceMode); + mPreferredAvdSelector.setEnabled(!deviceMode); + + if (deviceMode) { + handleDeviceSelection(); + } else { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected().getAvd()); + } + + enableOkButton(); + } + }); + mDeviceRadioButton.setSelection(true); + + + // offset the selector from the radio button + Composite offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); + GridData gd; + mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.heightHint = 100; + + mDeviceTable.setHeaderVisible(true); + mDeviceTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDeviceTable, "Serial Number", + SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "AVD Name", + SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "Target", + SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "Debug", + SWT.LEFT, "Debug", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "State", + SWT.LEFT, "bootloader", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + // create the viewer for it + mViewer = new TableViewer(mDeviceTable); + mViewer.setContentProvider(new ContentProvider()); + mViewer.setLabelProvider(new LabelProvider()); + mViewer.setInput(AndroidDebugBridge.getBridge()); + + mDeviceTable.addSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the device selector. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleDeviceSelection(); + } + + /** + * Handles double-click selection on the device selector. + * Note that the single-click handler will probably already have been called. + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handleDeviceSelection(); + if (isOkButtonEnabled()) { + okPressed(); + } + } + }); + + Button radio2 = new Button(top, SWT.RADIO); + radio2.setText("Launch a new Android Virtual Device"); + + // offset the selector from the radio button + offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mPreferredAvdSelector = new AvdSelector(offsetComp, + mSdkContext, + new NonRunningAvdFilter(), + AvdDisplayMode.SIMPLE_SELECTION); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setEnabled(false); + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the AVD selector. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (mDisableAvdSelectionChange == false) { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected().getAvd()); + enableOkButton(); + } + } + + /** + * Handles double-click selection on the AVD selector. + * + * Note that the single-click handler will probably already have been called + * but the selected item can have changed in between. + * + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + widgetSelected(e); + if (isOkButtonEnabled()) { + okPressed(); + } + } + }); + + return top; + } + + private void loadImages(Display display) { + ImageFactory imageFactory = mSdkContext.getSdkHelper().getImageFactory(); + + if (mDeviceImage == null) { + mDeviceImage = imageFactory.getImageByName("device.png"); //$NON-NLS-1$ + } + if (mEmulatorImage == null) { + mEmulatorImage = imageFactory.getImageByName("emulator.png"); //$NON-NLS-1$ + } + + if (mMatchImage == null) { + mMatchImage = mIconFactory.getColorIcon("match", //$NON-NLS-1$ + SWT.COLOR_DARK_GREEN); + } + + if (mNoMatchImage == null) { + mNoMatchImage = mIconFactory.getColorIcon("error", //$NON-NLS-1$ + SWT.COLOR_DARK_RED); + } + + if (mWarningImage == null) { + mWarningImage = mIconFactory.getColorIcon("warning", //$NON-NLS-1$ + SWT.COLOR_YELLOW); + } + + } + + /** + * 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 "??"; + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceConnected(IDevice) + */ + @Override + public void deviceConnected(IDevice device) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + @Override + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh all + mViewer.refresh(); + + // update the selection + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD.) + refillAvdList(false /*reloadAvds*/); + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + + } + }); + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(IDevice) + */ + @Override + public void deviceDisconnected(IDevice device) { + deviceConnected(device); + } + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + *

+ * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(IDevice, int) + */ + @Override + public void deviceChanged(final IDevice device, int changeMask) { + if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + @Override + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh the device + mViewer.refresh(device); + + // update the defaultSelection. + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD). This is done on deviceChanged because the avd name + // of a (emulator) device may be updated as the emulator boots. + + refillAvdList(false /*reloadAvds*/); + + // if the changed device is the current selection, + // we update the OK button based on its state. + if (device == mResponse.getDeviceToUse()) { + enableOkButton(); + } + + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + } + }); + } + } + + /** + * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). + */ + private boolean isDeviceMode() { + return mDeviceRadioButton.getSelection(); + } + + /** + * Enables or disables the OK button of the dialog based on various selections in the dialog. + */ + private void enableOkButton() { + Button okButton = getButton(IDialogConstants.OK_ID); + + if (isDeviceMode()) { + okButton.setEnabled(mResponse.getDeviceToUse() != null && + mResponse.getDeviceToUse().isOnline()); + } else { + okButton.setEnabled(mResponse.getAvdToLaunch() != null); + } + } + + /** + * Returns true if the ok button is enabled. + */ + private boolean isOkButtonEnabled() { + Button okButton = getButton(IDialogConstants.OK_ID); + return okButton.isEnabled(); + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mDeviceTable.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(this); + } + } + + private void handleDeviceSelection() { + int count = mDeviceTable.getSelectionCount(); + if (count != 1) { + handleSelection(null); + } else { + int index = mDeviceTable.getSelectionIndex(); + Object data = mViewer.getElementAt(index); + if (data instanceof IDevice) { + handleSelection((IDevice)data); + } else { + handleSelection(null); + } + } + } + + private void handleSelection(IDevice device) { + mResponse.setDeviceToUse(device); + enableOkButton(); + } + + /** + * Look for a default device to select. This is done by looking for the running + * clients on each device and finding one similar to the one being launched. + *

+ * This is done every time the device list changed unless there is a already selection. + */ + private void updateDefaultSelection() { + if (mDeviceTable.getSelectionCount() == 0) { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + + IDevice[] devices = bridge.getDevices(); + + for (IDevice device : devices) { + Client[] clients = device.getClients(); + + for (Client client : clients) { + + if (mPackageName.equals(client.getClientData().getClientDescription())) { + // found a match! Select it. + mViewer.setSelection(new StructuredSelection(device)); + handleSelection(device); + + // and we're done. + return; + } + } + } + } + + handleDeviceSelection(); + } + + private final class NonRunningAvdFilter implements IAvdFilter { + + private IDevice[] mDevices; + + @Override + public void prepare() { + mDevices = AndroidDebugBridge.getBridge().getDevices(); + } + + @Override + public boolean accept(AvdAgent avdAgent) { + AvdInfo info = avdAgent.getAvd(); + if (mDevices != null) { + for (IDevice d : mDevices) { + // do not accept running avd's + if (info.getName().equals(d.getAvdName())) { + return false; + } + + // only accept avd's that can actually run the project + Utilities.Compatibility c = + Utilities.canRun(avdAgent.getAvd(), getAndroidTargetFor(info), mProjectTarget, mMinApiVersion); + return (c == Utilities.Compatibility.NO) ? false : true; + } + } + + return true; + } + + @Override + public void cleanup() { + mDevices = null; + } + } + + /** + * Refills the AVD list keeping the current selection. + */ + private void refillAvdList(boolean reloadAvds) { + // save the current selection + AvdAgent selected = mPreferredAvdSelector.getSelected(); + + // disable selection change. + mDisableAvdSelectionChange = true; + + // refresh the list + mPreferredAvdSelector.refresh(false); + + // attempt to reselect the proper avd if needed + if (selected != null) { + if (mPreferredAvdSelector.setSelection(selected) == false) { + // looks like the selection is lost. this can happen if an emulator + // running the AVD that was selected was launched from outside of Eclipse). + mResponse.setAvdToLaunch(null); + enableOkButton(); + } + } + + // enable the selection change + mDisableAvdSelectionChange = false; + } + + private IAndroidTarget getAndroidTargetFor(AvdInfo info) { + SystemImage systemImage = (SystemImage)info.getSystemImage(); + return systemImage != null ? mSdkTargets.getTargetForSysImage(systemImage) : null; + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java new file mode 100644 index 00000000..c1e6a510 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java @@ -0,0 +1,158 @@ +/* + * 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.sdkuilib.ui; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Control; + +/** + * A little helper to create a new {@link GridData} and set its properties. + *

+ * Example of usage:
+ * + * GridDataHelper.create(myControl).hSpan(2).hAlignCenter().fill(); + * + */ +public final class GridDataBuilder { + + private GridData mGD; + + private GridDataBuilder() { + mGD = new GridData(); + } + + /** + * Creates new {@link GridData} and associates it on the control composite. + */ + static public GridDataBuilder create(Control control) { + GridDataBuilder gdh = new GridDataBuilder(); + control.setLayoutData(gdh.mGD); + return gdh; + } + + /** Sets widthHint to w. */ + public GridDataBuilder wHint(int w) { + mGD.widthHint = w; + return this; + } + + /** Sets heightHint to h. */ + public GridDataBuilder hHint(int h) { + mGD.heightHint = h; + return this; + } + + /** Sets horizontalIndent to h. */ + public GridDataBuilder hIndent(int h) { + mGD.horizontalIndent = h; + return this; + } + + /** Sets horizontalSpan to h. */ + public GridDataBuilder hSpan(int h) { + mGD.horizontalSpan = h; + return this; + } + + /** Sets verticalSpan to v. */ + public GridDataBuilder vSpan(int v) { + mGD.verticalSpan = v; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#CENTER}. */ + public GridDataBuilder hCenter() { + mGD.horizontalAlignment = SWT.CENTER; + return this; + } + + /** Sets verticalAlignment to {@link SWT#CENTER}. */ + public GridDataBuilder vCenter() { + mGD.verticalAlignment = SWT.CENTER; + return this; + } + + /** Sets verticalAlignment to {@link SWT#TOP}. */ + public GridDataBuilder vTop() { + mGD.verticalAlignment = SWT.TOP; + return this; + } + + /** Sets verticalAlignment to {@link SWT#BOTTOM}. */ + public GridDataBuilder vBottom() { + mGD.verticalAlignment = SWT.BOTTOM; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#LEFT}. */ + public GridDataBuilder hLeft() { + mGD.horizontalAlignment = SWT.LEFT; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#RIGHT}. */ + public GridDataBuilder hRight() { + mGD.horizontalAlignment = SWT.RIGHT; + return this; + } + + /** Sets horizontalAlignment to {@link GridData#FILL}. */ + public GridDataBuilder hFill() { + mGD.horizontalAlignment = GridData.FILL; + return this; + } + + /** Sets verticalAlignment to {@link GridData#FILL}. */ + public GridDataBuilder vFill() { + mGD.verticalAlignment = GridData.FILL; + return this; + } + + /** + * Sets both horizontalAlignment and verticalAlignment + * to {@link GridData#FILL}. + */ + public GridDataBuilder fill() { + mGD.horizontalAlignment = GridData.FILL; + mGD.verticalAlignment = GridData.FILL; + return this; + } + + /** Sets grabExcessHorizontalSpace to true. */ + public GridDataBuilder hGrab() { + mGD.grabExcessHorizontalSpace = true; + return this; + } + + /** Sets grabExcessVerticalSpace to true. */ + public GridDataBuilder vGrab() { + mGD.grabExcessVerticalSpace = true; + return this; + } + + /** + * Sets both grabExcessHorizontalSpace and + * grabExcessVerticalSpace to true. + */ + public GridDataBuilder grab() { + mGD.grabExcessHorizontalSpace = true; + mGD.grabExcessVerticalSpace = true; + return this; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java new file mode 100644 index 00000000..bf654204 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java @@ -0,0 +1,84 @@ +/* + * 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.sdkuilib.ui; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; + +/** + * JFace-based dialog that properly sets up a {@link GridLayout} top composite with the proper + * margin. + *

+ * Implementing dialog must create the content of the dialog in + * {@link #createDialogContent(Composite)}. + *

+ * A JFace dialog is perfect if you want a typical "OK | cancel" workflow, with the OK and + * cancel things all handled for you using a predefined layout. If you want a different set + * of buttons or a different layout, consider {@link SwtBaseDialog} instead. + */ +public abstract class GridDialog extends Dialog { + + private final int mNumColumns; + private final boolean mMakeColumnsEqualWidth; + + /** + * Creates the dialog + * @param parentShell the parent {@link Shell}. + * @param numColumns the number of columns in the grid + * @param makeColumnsEqualWidth whether or not the columns will have equal width + */ + public GridDialog(Shell parentShell, int numColumns, boolean makeColumnsEqualWidth) { + super(parentShell); + mNumColumns = numColumns; + mMakeColumnsEqualWidth = makeColumnsEqualWidth; + } + + /** + * Creates the content of the dialog. The parent composite is a {@link GridLayout} + * created with the numColumn and makeColumnsEqualWidth parameters + * passed to {@link #GridDialog(Shell, int, boolean)}. + *

+ * This is called by the parent's {@link #createContents(Composite)}. + * + * @param parent the parent composite. + */ + public abstract void createDialogContent(Composite parent); + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(mNumColumns, mMakeColumnsEqualWidth); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + layout.horizontalSpacing = convertHorizontalDLUsToPixels( + IDialogConstants.HORIZONTAL_SPACING); + top.setLayout(layout); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createDialogContent(top); + + applyDialogFont(top); + return top; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java new file mode 100644 index 00000000..6b9398f2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java @@ -0,0 +1,103 @@ +/* + * 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.sdkuilib.ui; + +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; + +/** + * A little helper to create a new {@link GridLayout}, associate to a {@link Composite} + * and set its common attributes. + *

+ * Example of usage:
+ * + * GridLayoutHelper.create(myComposite).noMargins().vSpacing(0).columns(2); + * + */ +public final class GridLayoutBuilder { + + private GridLayout mGL; + + private GridLayoutBuilder() { + mGL = new GridLayout(); + } + + /** + * Creates new {@link GridLayout} and associates it on the parent composite. + */ + static public GridLayoutBuilder create(Composite parent) { + GridLayoutBuilder glh = new GridLayoutBuilder(); + parent.setLayout(glh.mGL); + return glh; + } + + /** Sets all margins to 0. */ + public GridLayoutBuilder noMargins() { + mGL.marginHeight = 0; + mGL.marginWidth = 0; + mGL.marginLeft = 0; + mGL.marginTop = 0; + mGL.marginRight = 0; + mGL.marginBottom = 0; + return this; + } + + /** Sets all margins to n. */ + public GridLayoutBuilder margins(int n) { + mGL.marginHeight = n; + mGL.marginWidth = n; + mGL.marginLeft = n; + mGL.marginTop = n; + mGL.marginRight = n; + mGL.marginBottom = n; + return this; + } + + /** Sets numColumns to n. */ + public GridLayoutBuilder columns(int n) { + mGL.numColumns = n; + return this; + } + + /** Sets makeColumnsEqualWidth to true. */ + public GridLayoutBuilder columnsEqual() { + mGL.makeColumnsEqualWidth = true; + return this; + } + + /** Sets verticalSpacing to v. */ + public GridLayoutBuilder vSpacing(int v) { + mGL.verticalSpacing = v; + return this; + } + + /** Sets horizontalSpacing to h. */ + public GridLayoutBuilder hSpacing(int h) { + mGL.horizontalSpacing = h; + return this; + } + + /** + * Sets horizontalSpacing and verticalSpacing + * to s. + */ + public GridLayoutBuilder spacing(int s) { + mGL.verticalSpacing = s; + mGL.horizontalSpacing = s; + return this; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java new file mode 100644 index 00000000..a835014d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java @@ -0,0 +1,121 @@ +/* + * 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.sdkuilib.ui; + +import org.eclipse.jface.dialogs.IDialogConstants; +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.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.Monitor; +import org.eclipse.swt.widgets.Shell; + +/** + * Small dialog to let a user choose a screen size (from a fixed list) and a resolution + * (as returned by {@link Display#getMonitors()}). + + * After the dialog as returned, one can query {@link #getDensity()} to get the chosen monitor + * pixel density. + */ +public class ResolutionChooserDialog extends GridDialog { + public final static float[] MONITOR_SIZES = new float[] { + 13.3f, 14, 15.4f, 15.6f, 17, 19, 20, 21, 24, 30, + }; + + private Button mButton; + private Combo mScreenSizeCombo; + private Combo mMonitorCombo; + + private Monitor[] mMonitors; + private int mScreenSizeIndex = -1; + private int mMonitorIndex = 0; + + public ResolutionChooserDialog(Shell parentShell) { + super(parentShell, 2, false); + } + + /** + * Returns the pixel density of the user-chosen monitor. + */ + public int getDensity() { + float size = MONITOR_SIZES[mScreenSizeIndex]; + Rectangle rect = mMonitors[mMonitorIndex].getBounds(); + + // compute the density + double d = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / size; + return (int)Math.round(d); + } + + @Override + protected void configureShell(Shell newShell) { + newShell.setText("Monitor Density"); + super.configureShell(newShell); + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + mButton = getButton(IDialogConstants.OK_ID); + mButton.setEnabled(false); + return control; + } + + @Override + public void createDialogContent(Composite parent) { + Label l = new Label(parent, SWT.NONE); + l.setText("Screen Size:"); + + mScreenSizeCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + for (float size : MONITOR_SIZES) { + if (Math.round(size) == size) { + mScreenSizeCombo.add(String.format("%.0f\"", size)); + } else { + mScreenSizeCombo.add(String.format("%.1f\"", size)); + } + } + mScreenSizeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mScreenSizeIndex = mScreenSizeCombo.getSelectionIndex(); + mButton.setEnabled(mScreenSizeIndex != -1); + } + }); + + l = new Label(parent, SWT.NONE); + l.setText("Resolution:"); + + mMonitorCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + mMonitors = parent.getDisplay().getMonitors(); + for (Monitor m : mMonitors) { + Rectangle r = m.getBounds(); + mMonitorCombo.add(String.format("%d x %d", r.width, r.height)); + } + mMonitorCombo.select(mMonitorIndex); + mMonitorCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + mMonitorIndex = mMonitorCombo.getSelectionIndex(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java new file mode 100644 index 00000000..acc8cf3d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java @@ -0,0 +1,250 @@ +/* + * 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.sdkuilib.ui; + +import com.android.SdkConstants; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.util.HashMap; +import java.util.Map; + +/** + * A base class for an SWT Dialog. + *

+ * The base class offers the following goodies:
+ * - Dialog is automatically centered on its parent.
+ * - Dialog size is reused during the session.
+ * - A simple API with an {@link #open()} method that returns a boolean.
+ *

+ * A typical usage is: + *

+ *   MyDialog extends SwtBaseDialog { ... }
+ *   MyDialog d = new MyDialog(parentShell, "My Dialog Title");
+ *   if (d.open()) {
+ *      ...do something like refresh parent list view
+ *   }
+ * 
+ * We also have a JFace-base {@link GridDialog}. + * The JFace dialog is good when you just want a typical OK/Cancel layout with the + * buttons all managed for you. + * This SWT base dialog has little decoration. + * It's up to you to manage whatever buttons you want, if any. + */ +public abstract class SwtBaseDialog extends Dialog { + + /** + * Min Y location for dialog. Need to deal with the menu bar on mac os. + */ + private final static int MIN_Y = + SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0; + + /** Last dialog size for this session, different for each dialog class. */ + private static Map, Point> sLastSizeMap = new HashMap, Point>(); + + private volatile boolean mQuitRequested = false; + private boolean mReturnValue; + private Shell mShell; + + /** + * Create the dialog. + * + * @param parent The parent's shell + * @param title The dialog title. Can be null. + */ + public SwtBaseDialog(Shell parent, int swtStyle, String title) { + super(parent, swtStyle); + if (title != null) { + setText(title); + } + } + + /** + * Open the dialog. + * + * @return The last value set using {@link #setReturnValue(boolean)} or false by default. + */ + public boolean open() { + mReturnValue = false; + if (!mQuitRequested) { + createShell(); + } + if (!mQuitRequested) { + createContents(); + } + if (!mQuitRequested) { + positionShell(); + } + if (!mQuitRequested) { + postCreate(); + } + if (!mQuitRequested) { + mShell.open(); + mShell.layout(); + eventLoop(); + } + return mReturnValue; + } + + /** + * Creates the shell for this dialog. + * The default shell has a size of 450x300, which is also its minimum size. + * You might want to override these values. + *

+ * Called before {@link #createContents()}. + */ + protected void createShell() { + mShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL); + mShell.setMinimumSize(new Point(450, 300)); + mShell.setSize(450, 300); + if (getText() != null) { + mShell.setText(getText()); + } + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + saveSize(); + } + }); + } + + /** + * Creates the content and attaches it to the current shell (cf. {@link #getShell()}). + *

+ * Derived classes should consider creating the UI here and initializing their + * state in {@link #postCreate()}. + */ + protected abstract void createContents(); + + /** + * Called after {@link #createContents()} and after {@link #positionShell()} + * just before the dialog is actually shown on screen. + *

+ * Derived classes should consider creating the UI in {@link #createContents()} and + * initialize it here. + */ + protected abstract void postCreate(); + + /** + * Run the event loop. + * This is called from {@link #open()} after {@link #postCreate()} and + * after the window has been shown on screen. + * Derived classes might want to use this as a place to start automated + * tasks that will update the UI. + */ + protected void eventLoop() { + Display display = getParent().getDisplay(); + while (!mQuitRequested && !mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } + + /** + * Returns the current value that {@link #open()} will return to the caller. + * Default is false. + */ + protected boolean getReturnValue() { + return mReturnValue; + } + + /** + * Sets the value that {@link #open()} will return to the caller. + * @param returnValue The new value to be returned by {@link #open()}. + */ + protected void setReturnValue(boolean returnValue) { + mReturnValue = returnValue; + } + + /** + * Returns the shell created by {@link #createShell()}. + * @return The current {@link Shell}. + */ + protected Shell getShell() { + return mShell; + } + + /** + * Saves the dialog size and close the dialog. + * The {@link #open()} method will given return value (see {@link #setReturnValue(boolean)}. + *

+ * It's safe to call this method before the shell is initialized, + * in which case the dialog will close as soon as possible. + */ + protected void close() { + if (mShell != null && !mShell.isDisposed()) { + saveSize(); + getShell().close(); + } + mQuitRequested = true; + } + + //------- + + /** + * Centers the dialog in its parent shell. + */ + private void positionShell() { + // Centers the dialog in its parent shell + Shell child = mShell; + Shell parent = getParent(); + if (child != null && parent != null) { + // get the parent client area with a location relative to the display + Rectangle parentArea = parent.getClientArea(); + Point parentLoc = parent.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + // Reuse the last size if there's one, otherwise use the default + Point childSize = sLastSizeMap.get(this.getClass()); + if (childSize == null) { + childSize = child.getSize(); + } + int cw = childSize.x; + int ch = childSize.y; + + int x = px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < MIN_Y) y = MIN_Y; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + mShell.layout(true, true); + final Point newSize = mShell.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); + mShell.setSize(newSize); + } + + private void saveSize() { + if (mShell != null && !mShell.isDisposed()) { + sLastSizeMap.put(this.getClass(), mShell.getSize()); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java new file mode 100644 index 00000000..e7b9d4c8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java @@ -0,0 +1,151 @@ +/* + * 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.sdkuilib.widgets; + +import com.android.annotations.NonNull; +import com.android.sdkuilib.internal.widgets.IMessageBoxLogger; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; + + +/** + * Collects all log and displays it in a message box dialog. + *

+ * This is good if only a few lines of log are expected. + * If you pass logErrorsOnly to the constructor, the message box + * will be shown only if errors were generated, which is the typical use-case. + *

+ * To use this:
+ * - Construct a new {@link MessageBoxLog}.
+ * - Pass the logger to the action.
+ * - Once the action is completed, call {@link #displayResult(boolean)} + * indicating whether the operation was successful or not. + * + * When logErrorsOnly is true, if the operation was not successful or errors + * were generated, this will display the message box. + */ +public final class MessageBoxLog implements IMessageBoxLogger { + + final ArrayList logMessages = new ArrayList(); + private final String mMessage; + private final Display mDisplay; + private final boolean mLogErrorsOnly; + + /** + * Creates a logger that will capture all logs and eventually display them + * in a simple message box. + * + * @param message + * @param display + * @param logErrorsOnly + */ + public MessageBoxLog(String message, Display display, boolean logErrorsOnly) { + mMessage = message; + mDisplay = display; + mLogErrorsOnly = logErrorsOnly; + } + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + if (errorFormat != null) { + logMessages.add(String.format("Error: " + errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format("Warning: " + warningFormat, arg)); + } + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format(msgFormat, arg)); + } + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format(msgFormat, arg)); + } + } + + /** + * Displays the log if anything was captured. + *

+ * @param success Used only when the logger was constructed with logErrorsOnly==true. + * In this case the dialog will only be shown either if success if false or some errors + * where captured. + */ + @Override + public void displayResult(final boolean success) { + if (logMessages.size() > 0) { + final StringBuilder sb = new StringBuilder(mMessage + "\n\n"); + for (String msg : logMessages) { + if (msg.length() > 0) { + if (msg.charAt(0) != '\n') { + int n = sb.length(); + if (n > 0 && sb.charAt(n-1) != '\n') { + sb.append('\n'); + } + } + sb.append(msg); + } + } + + // display the message + // dialog box only run in ui thread.. + if (mDisplay != null && !mDisplay.isDisposed()) { + mDisplay.asyncExec(new Runnable() { + @Override + public void run() { + // This is typically displayed at the end, so make sure the UI + // instances are not disposed. + Shell shell = null; + if (mDisplay != null && !mDisplay.isDisposed()) { + shell = mDisplay.getActiveShell(); + } + if (shell == null || shell.isDisposed()) { + return; + } + // Use the success icon if the call indicates success. + // However just use the error icon if the logger was only recording errors. + if (success && !mLogErrorsOnly) { + MessageDialog.openInformation(shell, "Android Virtual Devices Manager", + sb.toString()); + } else { + MessageDialog.openError(shell, "Android Virtual Devices Manager", + sb.toString()); + + } + } + }); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java new file mode 100644 index 00000000..27e009e0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java @@ -0,0 +1,463 @@ +/* + * 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.sdkuilib.widgets; + +import com.android.SdkConstants; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +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.Control; +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.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; +import java.util.Comparator; + + +/** + * The SDK target selector is a table that is added to the given parent composite. + *

+ * To use, create it using {@link #SdkTargetSelector(Composite, IAndroidTarget[], boolean)} then + * call {@link #setSelection(IAndroidTarget)}, {@link #setSelectionListener(SelectionListener)} + * and finally use {@link #getSelected()} to retrieve the + * selection. + */ +public class SdkTargetSelector { + + private IAndroidTarget[] mTargets; + private final boolean mAllowSelection; + private SelectionListener mSelectionListener; + private Table mTable; + private Label mDescription; + private Composite mInnerGroup; + + /** Cache for {@link #getCheckboxWidth()} */ + private static int sCheckboxWidth = -1; + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets) { + this(parent, targets, true /*allowSelection*/); + } + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowSelection True if selection is enabled. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, boolean allowSelection) { + // Layout has 1 column + mInnerGroup = new Composite(parent, SWT.NONE); + mInnerGroup.setLayout(new GridLayout()); + mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + mInnerGroup.setFont(parent.getFont()); + + mAllowSelection = allowSelection; + int style = SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION; + if (allowSelection) { + style |= SWT.CHECK; + } + mTable = new Table(mInnerGroup, style); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + + mDescription = new Label(mInnerGroup, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("Target Name"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Vendor"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("Platform"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("API Level"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3); + setupSelectionListener(mTable); + setTargets(targets); + setupTooltip(mTable); + } + + /** + * Returns the layout data of the inner composite widget that contains the target selector. + * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH} + * mode. + *

+ * This can be useful if you want to change the {@link GridData#horizontalSpan} for example. + */ + public Object getLayoutData() { + return mInnerGroup.getLayoutData(); + } + + /** + * Returns the list of known targets. + *

+ * This is not a copy. Callers must not modify this array. + */ + public IAndroidTarget[] getTargets() { + return mTargets; + } + + /** + * Changes the targets of the SDK Target Selector. + * + * @param targets The list of targets. This is not copied, the caller must not modify. + */ + public void setTargets(IAndroidTarget[] targets) { + mTargets = targets; + if (mTargets != null) { + Arrays.sort(mTargets, new Comparator() { + @Override + public int compare(IAndroidTarget o1, IAndroidTarget o2) { + return o1.compareTo(o2); + } + }); + } + + fillTable(mTable); + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selection + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(IAndroidTarget target) { + if (!mAllowSelection) { + return false; + } + + boolean found = false; + boolean modified = false; + + if (mTable != null && !mTable.isDisposed()) { + for (TableItem i : mTable.getItems()) { + if ((IAndroidTarget) i.getData() == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } + } + + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + + return found; + } + + /** + * Returns the selected item. + * + * @return The selected item or null. + */ + public IAndroidTarget getSelected() { + if (mTable == null || mTable.isDisposed()) { + return null; + } + + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (IAndroidTarget) i.getData(); + } + } + return null; + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + int width = r.width; + + // On the Mac, the width of the checkbox column is not included (and checkboxes + // are shown if mAllowSelection=true). Subtract this size from the available + // width to be distributed among the columns. + if (mAllowSelection + && SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { + width -= getCheckboxWidth(); + } + + column0.setWidth(width * 30 / 100); // 30% + column1.setWidth(width * 45 / 100); // 45% + column2.setWidth(width * 15 / 100); // 15% + column3.setWidth(width * 10 / 100); // 10% + } + }); + } + + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + if (!mAllowSelection) { + return; + } + + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + } + + /** + * If we're not in multiple selection mode, uncheck all other + * items when this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }); + } + + + /** + * Fills the table with all SDK targets. + * The table columns are: + *

    + *
  • column 0: sdk name + *
  • column 1: sdk vendor + *
  • column 2: sdk platform + *
  • column 3: sdk version + *
+ */ + private void fillTable(final Table table) { + + if (table == null || table.isDisposed()) { + return; + } + + table.removeAll(); + + if (mTargets != null && mTargets.length > 0) { + table.setEnabled(true); + for (IAndroidTarget target : mTargets) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(target); + item.setText(0, target.getName()); + item.setText(1, target.getVendor()); + String platform = target.getVersionName(); + if (platform == null) + platform = ""; + item.setText(2, platform); + item.setText(3, target.getVersion().getApiString()); + } + } else { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No target available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + /** + * Sets up a tooltip that displays the current item description. + *

+ * Displaying a tooltip over the table looks kind of odd here. Instead we actually + * display the description in a label under the table. + */ + private void setupTooltip(final Table table) { + + if (table == null || table.isDisposed()) { + return; + } + + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + @Override + public void handleEvent(Event event) { + + switch(event.type) { + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + return; + + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + + default: + return; + } + + } + }; + + table.addListener(SWT.Dispose, listener); + table.addListener(SWT.KeyDown, listener); + table.addListener(SWT.MouseMove, listener); + table.addListener(SWT.MouseHover, listener); + } + + /** + * Updates the description label with the description of the item's android target, if any. + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof IAndroidTarget) { + String newTooltip = ((IAndroidTarget) data).getDescription(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } + } + + /** Enables or disables the controls. */ + public void setEnabled(boolean enabled) { + if (mInnerGroup != null && mTable != null && !mTable.isDisposed()) { + enableControl(mInnerGroup, enabled); + } + } + + /** Enables or disables controls; recursive for composite controls. */ + private void enableControl(Control c, boolean enabled) { + c.setEnabled(enabled); + if (c instanceof Composite) + for (Control c2 : ((Composite) c).getChildren()) { + enableControl(c2, enabled); + } + } + + /** Computes the width of a checkbox */ + private int getCheckboxWidth() { + if (sCheckboxWidth == -1) { + Shell shell = new Shell(mTable.getShell(), SWT.NO_TRIM); + Button checkBox = new Button(shell, SWT.CHECK); + sCheckboxWidth = checkBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; + shell.dispose(); + } + + return sCheckboxWidth; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java new file mode 100644 index 00000000..e76c0687 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java @@ -0,0 +1,99 @@ +/** + * + */ +package org.eclipse.andmore.sdktool; + +import org.eclipse.andmore.base.resources.IEditorIconFactory; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.graphics.Image; + +import com.android.repository.api.RepoManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.utils.ILogger; + +/** + * @author andrew + * + */ +public class SdkCallAgent { + public static final int NO_TOOLS_MSG = 0; + public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; + public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; + + private final SdkContext sdkContext; + private final ILogger consoleLogger; + private IEditorIconFactory iconEditorFactory; + + /** + * Construct SdkCallAgent object to mediate between application and UI layer + * @param sdkHandler SDK handler + * @param repoManager Repository manager + * @param consoleLogger Console logger to persist all messages + */ + public SdkCallAgent( + AndroidSdkHandler sdkHandler, + RepoManager repoManager, + ILogger consoleLogger) + { + this.sdkContext = new SdkContext(sdkHandler, repoManager); + sdkContext.setSdkLogger(consoleLogger); + this.consoleLogger = consoleLogger; + } + + /** + * Construct SdkCallAgent object to mediate between application and UI layer requiring an icon factory + * @param sdkHandler SDK handler + * @param repoManager Repository manager + * @param iconEditorFactory Icon factory to provide editor icons + * @param consoleLogger Console logger to persist all messages + */ + public SdkCallAgent( + AndroidSdkHandler sdkHandler, + RepoManager repoManager, + IEditorIconFactory iconEditorFactory, + ILogger consoleLogger) + { + this(sdkHandler, repoManager, consoleLogger); + this.iconEditorFactory = iconEditorFactory; + } + + public SdkContext getSdkContext() { + SdkHelper helper = sdkContext.getSdkHelper(); + if (helper.getImageFactory() == null) + helper.setImageFactory(getImageLoader()); + return sdkContext; + } + + public IEditorIconFactory getEditorIconFactory() { + if (iconEditorFactory ==null) + // Icon factory not set. Do not throw exception, but handle gracefully. + return new IEditorIconFactory(){ + + @Override + public Image getColorIcon(String osName, int color) { + // Return generic image to avoid NPE + return sdkContext.getSdkHelper().getImageByName("nopkg_icon_16.png"); + }};// + return iconEditorFactory; + } + + /** + * Set image loader if not already set + */ + public ImageFactory getImageLoader() + { + SdkUserInterfacePlugin sdkPlugin = SdkUserInterfacePlugin.instance(); + return sdkPlugin != null ? sdkPlugin.getImageFactory() : null; + } + + /** + * Call completeOperations() in a finally clause to ensure pending notifications are triggered + */ + public void completeSdkOperations() + { + SdkHelper helper = sdkContext.getSdkHelper(); + if (sdkContext.isSdkLocationChanged() || helper.isReloadPending()) + helper.broadcastOnSdkReload(consoleLogger); + + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java new file mode 100644 index 00000000..acedeb92 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java @@ -0,0 +1,222 @@ +package org.eclipse.andmore.sdktool; + +import java.io.File; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressIndicatorAdapter; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.io.FileOp; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.sdkuilib.internal.repository.PackageManager; +import com.android.sdkuilib.internal.repository.Settings; +import com.android.utils.ILogger; + +public class SdkContext { + + private final AndroidSdkHandler handler; + private final RepoManager repoManager; + private final DeviceManager deviceManager; + private final PackageManager packageManager; + private final SdkHelper sdkHelper; + private final AtomicBoolean hasWarning = new AtomicBoolean(); + private final AtomicBoolean hasError = new AtomicBoolean(); + private final ArrayList logMessages = new ArrayList(); + private Settings settings; + private ProgressIndicator sdkProgressIndicator; + private ILogger sdkLogger; + private boolean sdkLocationChanged; + + public SdkContext(AndroidSdkHandler handler, RepoManager repoManager) { + super(); + this.handler = handler; + this.repoManager = repoManager; + this.sdkHelper = new SdkHelper(); + deviceManager = DeviceManager.createInstance(handler.getLocation(), loggerInstance()); + packageManager = new PackageManager(this); + } + + public void setSettings(Settings settings) + { + this.settings = settings; + } + + public Settings getSettings() + { + if (settings == null) + settings = new Settings(); + return settings; + } + + public boolean isSdkLocationChanged() { + return sdkLocationChanged; + } + + public PackageManager getPackageManager() + { + return packageManager; + } + public SdkHelper getSdkHelper() + { + return sdkHelper; + } + + public AndroidSdkHandler getHandler() { + return handler; + } + + public RepoManager getRepoManager() { + return repoManager; + } + + public AvdManager getAvdManager() + { + String avdFolder = null; + AvdManager avdManager = null; + ILogger logger = loggerInstance(); + try { + avdFolder = AndroidLocation.getAvdFolder(); + avdManager = AvdManager.getInstance(handler, new File(avdFolder), logger); + } catch (AndroidLocation.AndroidLocationException e) { + logger.error(e, "Error obtaining AVD Manager"); + } + return avdManager; + } + + public RepositoryPackages getPackages() { + return repoManager.getPackages(); + } + + public FileOp getFileOp() { + return handler.getFileOp(); + } + + public File getLocalPath() { + return repoManager.getLocalPath(); + } + + public File getLocation() { + File location = handler.getLocation(); + if (location == null) + return new File(""); + return location; + } + + public Map getRemotePackages() { + return getPackages().getRemotePackages(); + } + + public Map getLocalPackages() { + return getPackages().getLocalPackages(); + } + + public DeviceManager getDeviceManager() { + return deviceManager; + } + + public boolean hasWarning() { + return hasWarning.get(); + } + + public boolean hasError() { + return hasError.get(); + } + + public ArrayList getLogMessages() { + return logMessages; + } + + public ILogger loggerInstance() { + hasWarning.set(false); + hasError.set(false); + logMessages.clear(); + return new ILogger() { + @Override + public void error(@Nullable Throwable throwable, @Nullable String errorFormat, + Object... arg) { + hasError.set(true); + if (errorFormat != null) { + logMessages.add(String.format("Error: " + errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + hasWarning.set(true); + logMessages.add(String.format("Warning: " + warningFormat, arg)); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + logMessages.add(String.format(msgFormat, arg)); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + info(msgFormat, arg); + } + }; + + } + + public void setSdkProgressIndicator(ProgressIndicator sdkProgressIndicator) { + this.sdkProgressIndicator = sdkProgressIndicator; + } + + public void setSdkLogger(ILogger sdkLogger) { + this.sdkLogger = sdkLogger; + } + + public ProgressIndicator getProgressIndicator() { + if (sdkProgressIndicator != null) + return sdkProgressIndicator; + return new ProgressIndicatorAdapter() + { + ILogger logger = getSdkLog(); + @Override + public void logWarning(@NonNull String s, @Nullable Throwable e) { + if (s != null) + logger.warning(s); + if (e != null) + logger.warning(e.getMessage()); + } + + @Override + public void logError(@NonNull String s, @Nullable Throwable e) { + logger.error(e, s); + } + + @Override + public void logInfo(@NonNull String s) { + logger.info(s); + } + }; + } + + public ILogger getSdkLog() { + if (sdkLogger != null) + return sdkLogger; + return loggerInstance(); + } + + public void setLocation(File sdkLocation) { + // This change needs to be monitored so external AndroidSdkHandler consumers can re-sync + sdkLocationChanged = true; + AndroidSdkHandler.resetInstance(sdkLocation); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java new file mode 100644 index 00000000..cb916169 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java @@ -0,0 +1,153 @@ +package org.eclipse.andmore.sdktool; + +import java.util.ArrayList; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.utils.ILogger; + +public class SdkHelper { + private Shell mWindowShell; + + /** The current {@link ImageFactory}. */ + private ImageFactory mImageFactory; + /** Flag to remember one or more packages have been installed. Reset on {@link #broadcastOnSdkReload(ILogger)} called. */ + private boolean isReloadPending; + + private final ArrayList mListeners = new ArrayList(); + + public boolean isReloadPending() { + return isReloadPending; + } + + /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void addListeners(ISdkChangeListener listener) { + if (mListeners.contains(listener) == false) { + mListeners.add(listener); + } + } + + /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void removeListener(ISdkChangeListener listener) { + mListeners.remove(listener); + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#onSdkReload()}. + * This can be called from any thread. + */ + public void broadcastOnSdkReload(ILogger logger) { + isReloadPending = false; + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.onSdkReload(); + } catch (Throwable t) { + logger.error(t, null); + } + } + } + }); + } + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#preInstallHook()}. + * This can be called from any thread. + */ + public void broadcastPreInstallHook(ILogger logger) { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.preInstallHook(); + } catch (Throwable t) { + logger.error(t, null); + } + } + } + }); + } + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#postInstallHook()}. + * This can be called from any thread. + */ + public void broadcastPostInstallHook(ILogger logger) { + if (!mListeners.isEmpty()) { + isReloadPending = true; + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.postInstallHook(); + } catch (Throwable t) { + logger.error(t, null); + } + } + } + }); + } + } + + public void setWindowShell(Shell windowShell) { + mWindowShell = windowShell; + } + + public Shell getWindowShell() { + return mWindowShell; + } + + public void setImageFactory(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + /** + * Returns image factory. + * @return ImageFactory object + */ + public ImageFactory getImageFactory() { + return mImageFactory; + } + + /** + * Loads an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * The image is cached. Successive calls will return the same object.
+ * The image is automatically disposed when {@link ImageFactory} is disposed. + * + * @param imageName The filename (with extension) of the image to load. + * @return A new or existing {@link Image}. The caller must NOT dispose the image. + * The returned image can be null if the expected file is missing. + */ + @Nullable + public Image getImageByName(@NonNull String imageName) { + return mImageFactory != null ? mImageFactory.getImageByName(imageName, imageName, null) : null; + } + + /** + * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. + * + * @param r Non-null runnable. + */ + protected void runOnUiThread(@NonNull Runnable r) { + if (mWindowShell != null && !mWindowShell.isDisposed()) { + mWindowShell.getDisplay().syncExec(r); + } + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java new file mode 100644 index 00000000..c1f49235 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.sdktool; + +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.jface.resource.ImageDescriptor; + +public class SdkResourceProvider implements PluginResourceProvider { + + @Override + public ImageDescriptor descriptorFromPath(String imagePath) { + return SdkUserInterfacePlugin.getImageDescriptor(imagePath); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java new file mode 100644 index 00000000..da2455ea --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.sdktool; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * Plugin activator to manage resources such as images + * @author Andrew Bowley + * + */ +public class SdkUserInterfacePlugin extends AbstractUIPlugin { + + public static final String PLUGIN_ID = "org.eclipse.andmore.sdkuilib"; //$NON-NLS-1$ + + private static SdkUserInterfacePlugin instance; + private ImageFactory imageFactory; + + public SdkUserInterfacePlugin() { + super(); + instance = this; + } + + public ImageFactory getImageFactory() { + return imageFactory; + } + + public static SdkUserInterfacePlugin instance() + { + return instance; + } + + /** + * Starts up this plug-in. + *

+ * This method should be overridden in subclasses that need to do something + * when this plug-in is started. Implementors should call the inherited method + * at the first possible point to ensure that any system requirements can be met. + *

+ *

+ * If this method throws an exception, it is taken as an indication that + * plug-in initialization has failed; as a result, the plug-in will not + * be activated; moreover, the plug-in will be marked as disabled and + * ineligible for activation for the duration. + *

+ *

+ * Note 1: This method is automatically invoked by the platform + * the first time any code in the plug-in is executed. + *

+ *

+ * Note 2: This method is intended to perform simple initialization + * of the plug-in environment. The platform may terminate initializers + * that do not complete in a timely fashion. + *

+ *

+ * Note 3: The class loader typically has monitors acquired during invocation of this method. It is + * strongly recommended that this method avoid synchronized blocks or other thread locking mechanisms, + * as this would lead to deadlock vulnerability. + *

+ *

+ * Note 4: The supplied bundle context represents the plug-in to the OSGi framework. + * For security reasons, it is strongly recommended that this object should not be divulged. + *

+ *

+ * Note 5: This method and the {@link #stop(BundleContext)} may be called from separate threads, + * but the OSGi framework ensures that both methods will not be called simultaneously. + *

+ * Clients must never explicitly call this method. + * + * @param context the bundle context for this plug-in + * @exception Exception if this plug-in did not start up properly + * @since 3.0 + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + imageFactory = new JFaceImageLoader( + new PluginResourceProvider() { + + @Override + public ImageDescriptor descriptorFromPath(String imagePath) { + return getImageDescriptor(imagePath); + } + }); + } + + /** + * Stops this plug-in. + *

+ * This method should be re-implemented in subclasses that need to do something + * when the plug-in is shut down. Implementors should call the inherited method + * as late as possible to ensure that any system requirements can be met. + *

+ *

+ * Plug-in shutdown code should be robust. In particular, this method + * should always make an effort to shut down the plug-in. Furthermore, + * the code should not assume that the plug-in was started successfully. + *

+ *

+ * Note 1: If a plug-in has been automatically started, this method will be automatically + * invoked by the platform when the platform is shut down. + *

+ *

+ * Note 2: This method is intended to perform simple termination + * of the plug-in environment. The platform may terminate invocations + * that do not complete in a timely fashion. + *

+ *

+ * Note 3: The supplied bundle context represents the plug-in to the OSGi framework. + * For security reasons, it is strongly recommended that this object should not be divulged. + *

+ *

+ * Note 4: This method and the {@link #start(BundleContext)} may be called from separate threads, + * but the OSGi framework ensures that both methods will not be called simultaneously. + *

+ * Clients must never explicitly call this method. + * + * @param context the bundle context for this plug-in + * @exception Exception if this method fails to shut down this plug-in + * @since 3.0 + */ + @Override + public void stop(BundleContext context) throws Exception { + imageFactory.dispose(); + super.stop(context); + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java new file mode 100644 index 00000000..50401ee7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + */ +package org.eclipse.andmore.sdktool; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.IDevice; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; + +/** + * @author Andrew Bowley + * + * 11-11-2017 + */ +public class Utilities { + public enum Compatibility { + YES, + NO, + UNKNOWN, + }; + + /** + * Format file size given value as number of bytes. + * Taken from deprecated Archive class + * @param size Number of bytes + * @return text size formatted according to scale up to gigabytes + */ + public static String formatFileSize(long size) { + String sizeStr; + if (size < 1024) { + sizeStr = String.format("%d Bytes", size); + } else if (size < 1024 * 1024) { + sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); + } else if (size < 1024 * 1024 * 1024) { + sizeStr = String.format("%.1f MiB", + Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); + } else { + sizeStr = String.format("%.1f GiB", + Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); + } + + return String.format("Size: %1$s", sizeStr); + } + + + @Nullable + public static AndroidVersion getDeviceVersion(@NonNull IDevice device) { + try { + Future future = device.getSystemProperty(IDevice.PROP_BUILD_API_LEVEL); + String apiLevel = null; + apiLevel = future.get(); + if (apiLevel == null) { + return null; + } + future = device.getSystemProperty(IDevice.PROP_BUILD_CODENAME); + return new AndroidVersion(Integer.parseInt(apiLevel), + future.get()); + } catch (NumberFormatException | InterruptedException | ExecutionException e) { + return null; + } + } + + /** + * Returns whether the specified AVD can run the given project that is built against + * a particular SDK and has the specified minApiLevel. + * @param avd AVD to check compatibility for + * @param avdTarget AVD target + * @param projectTarget project build target + * @param minApiVersion project min api level + * @return whether the given AVD can run the given application + */ + public static Compatibility canRun(AvdInfo avd, IAndroidTarget avdTarget, IAndroidTarget projectTarget, + AndroidVersion minApiVersion) { + if (avd == null) { + return Compatibility.UNKNOWN; + } + + if (avdTarget == null) { + return Compatibility.UNKNOWN; + } + + // for platform targets, we only need to check the min api version + if (projectTarget.isPlatform()) { + return avdTarget.getVersion().canRun(minApiVersion) ? + Compatibility.YES : Compatibility.NO; + } + + // for add-on targets, delegate to the add on target to check for compatibility + return projectTarget.canRunOn(avdTarget) ? Compatibility.YES : Compatibility.NO; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swt/.classpath b/andmore-swt/org.eclipse.andmore.swt/.classpath new file mode 100644 index 00000000..292b6281 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.classpath @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/.gitignore b/andmore-swt/org.eclipse.andmore.swt/.gitignore new file mode 100644 index 00000000..f4f27d8a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.gitignore @@ -0,0 +1,2 @@ +/target/ +/libs/ \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/.project b/andmore-swt/org.eclipse.andmore.swt/.project new file mode 100644 index 00000000..872dc77f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.swt + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..c137e176 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF new file mode 100644 index 00000000..20d3c600 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF @@ -0,0 +1,118 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.swt;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.apache.commons.logging;bundle-version="1.1.1", + com.google.gson;bundle-version="2.2.4" +Bundle-ActivationPolicy: lazy +Bundle-Vendor: %Bundle-Vendor +Bundle-ClassPath: ., + libs/annotations-25.3.3.jar, + libs/common-25.3.3.jar, + libs/guava-18.0.jar, + libs/httpmime-4.1.jar, + libs/kxml2-2.3.0.jar, + libs/layoutlib-api-25.3.3.jar, + libs/sdklib-25.3.3.jar, + libs/dvlib-25.3.3.jar, + libs/sdk-common-25.3.3.jar, + libs/repository-25.3.3.jar, + libs/ddmlib-25.3.3.jar, + libs/jimfs-1.1.jar, + libs/commons-compress-1.8.1.jar +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: com.android, + com.android.annotations, + com.android.annotations.concurrency, + com.android.ddmlib, + com.android.ddmlib.log, + com.android.ddmlib.logcat, + com.android.ddmlib.testrunner, + com.android.ddmlib.utils, + com.android.dvlib, + com.android.ide.common.blame, + com.android.ide.common.internal, + com.android.ide.common.rendering, + com.android.ide.common.rendering.api, + com.android.ide.common.repository, + com.android.ide.common.res2, + com.android.ide.common.resources, + com.android.ide.common.resources.configuration, + com.android.ide.common.sdk, + com.android.ide.common.util, + com.android.ide.common.xml, + com.android.io, + com.android.prefs, + com.android.repository, + com.android.repository.api, + com.android.repository.impl.meta, + com.android.repository.io, + com.android.repository.io.impl, + com.android.repository.testframework, + com.android.repository.util, + com.android.resources, + com.android.sdklib, + com.android.sdklib.build, + com.android.sdklib.devices, + com.android.sdklib.internal.avd, + com.android.sdklib.internal.build, + com.android.sdklib.internal.project, + com.android.sdklib.repository, + com.android.sdklib.repository.installer, + com.android.sdklib.repository.legacy, + com.android.sdklib.repository.legacy.remote.internal, + com.android.sdklib.repository.meta, + com.android.sdklib.repository.targets, + com.android.sdklib.util, + com.android.util, + com.android.utils, + com.android.xml, + com.google.common.annotations, + com.google.common.base, + com.google.common.base.internal, + com.google.common.cache, + com.google.common.collect, + com.google.common.eventbus, + com.google.common.hash, + com.google.common.io, + com.google.common.math, + com.google.common.net, + com.google.common.primitives, + com.google.common.reflect, + com.google.common.util.concurrent, + org.apache.http.entity.mime, + org.apache.http.entity.mime.content, + org.eclipse.andmore.base, + org.eclipse.andmore.base.resources, + org.kxml2.io, + org.kxml2.kdom, + org.kxml2.wap, + org.kxml2.wap.syncml, + org.kxml2.wap.wml, + org.kxml2.wap.wv, + org.xmlpull.v1, + org.apache.commons.compress.archivers, + org.apache.commons.compress.archivers.ar, + org.apache.commons.compress.archivers.arj, + org.apache.commons.compress.archivers.cpio, + org.apache.commons.compress.archivers.dump, + org.apache.commons.compress.archivers.jar, + org.apache.commons.compress.archivers.sevenz, + org.apache.commons.compress.archivers.tar, + org.apache.commons.compress.archivers.zip, + org.apache.commons.compress.changes, + org.apache.commons.compress.compressors, + org.apache.commons.compress.compressors.bzip2, + org.apache.commons.compress.compressors.gzip, + org.apache.commons.compress.compressors.lzma, + org.apache.commons.compress.compressors.pack200, + org.apache.commons.compress.compressors.snappy, + org.apache.commons.compress.compressors.xz, + org.apache.commons.compress.compressors.z, + org.apache.commons.compress.utils diff --git a/andmore-swt/org.eclipse.andmore.swt/about.html b/andmore-swt/org.eclipse.andmore.swt/about.html new file mode 100644 index 00000000..52791768 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/about.html @@ -0,0 +1,29 @@ + + + + + +About + + +

About This Content

+ +

March 31, 2015

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/build.properties b/andmore-swt/org.eclipse.andmore.swt/build.properties new file mode 100644 index 00000000..b47d897d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/build.properties @@ -0,0 +1,9 @@ +output.. = bin/ +bin.includes = .,\ + libs/,\ + META-INF/,\ + plugin.xml,\ + plugin.properties,\ + about.html +jars.compile.order = . +source.. = src/ diff --git a/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml b/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml new file mode 100644 index 00000000..5869a9b2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/plugin.properties b/andmore-swt/org.eclipse.andmore.swt/plugin.properties new file mode 100644 index 00000000..c93b97ad --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/plugin.properties @@ -0,0 +1,9 @@ +######################################################### +# +# Properties file +# +######################################################### + + +Bundle-Name=Common Android Utilities +Bundle-Vendor=Eclipse Andmore Project diff --git a/andmore-swt/org.eclipse.andmore.swt/plugin.xml b/andmore-swt/org.eclipse.andmore.swt/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/pom.xml b/andmore-swt/org.eclipse.andmore.swt/pom.xml new file mode 100644 index 00000000..2a91aaae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/pom.xml @@ -0,0 +1,236 @@ + + + 4.0.0 + + org.eclipse.andmore.swt + eclipse-plugin + swt base + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + com.android.tools.build + builder + ${android.builder.version} + + + com.android.tools.build + builder-model + + + com.android.tools.build + builder-test-api + + + com.android.tools.jack + jack-api + + + com.android.tools.jill + jill-api + + + com.android.tools.analytics-library + protos + + + com.android.tools.analytics-library + shared + + + com.android.tools.analytics-library + tracker + + + com.squareup + javawriter + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + org.ow2.asm + asm + + + org.ow2.asm + asm-tree + + + + + com.android.tools + sdklib + ${android.tools.version} + + + com.android.tools.ddms + ddmlib + ${android.tools.version} + + + com.android.tools + dvlib + ${android.tools.version} + + + com.android.tools + repository + ${android.tools.version} + + + com.android.tools + sdk-common + ${android.tools.version} + + + com.android.tools.build + builder-model + + + com.android.tools.build + builder-test-api + + + + + com.intellij + annotations + 12.0 + + + org.apache.commons + commons-compress + 1.8.1 + + + com.google.jimfs + jimfs + 1.1 + + + org.apache.httpcomponents + httpmime + 4.1 + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + maven-dependency-plugin + + + copy + initialize + + copy + + + + + + com.android.tools + common + ${android.tools.version} + + + net.sf.kxml + kxml2 + 2.3.0 + + + com.android.tools + annotations + ${android.tools.version} + + + com.google.guava + guava + 18.0 + + + com.android.tools + dvlib + ${android.tools.version} + + + com.android.tools + sdk-common + ${android.tools.version} + + + com.android.tools + sdklib + ${android.tools.version} + + + com.android.tools.ddms + ddmlib + ${android.tools.version} + + + com.android.tools.layoutlib + layoutlib-api + ${android.tools.version} + + + com.google.jimfs + jimfs + 1.1 + + + org.apache.httpcomponents + httpmime + 4.1 + + + com.android.tools + repository + ${android.tools.version} + + + org.apache.commons + commons-compress + 1.8.1 + + + ${project.basedir}/libs + false + false + true + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java new file mode 100644 index 00000000..7ef9cc02 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java @@ -0,0 +1,42 @@ +/* + * 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 org.eclipse.andmore.base; + +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; + +public class InstallDetails { + private static final String ANDMORE_PLUGIN_ID = "org.eclipse.andmore"; //$NON-NLS-1$ + private static final String ECLIPSE_PLATFORM_PLUGIN_ID = "org.eclipse.platform"; //$NON-NLS-1$ + + /** + * Returns true if the ADT plugin is available in the current platform. This + * is useful for distinguishing between specific RCP applications vs. ADT + + * Eclipse. + */ + public static boolean isAdtInstalled() { + Bundle b = Platform.getBundle(ANDMORE_PLUGIN_ID); + return b != null; + } + + /** Returns the version of current eclipse platform. */ + public static Version getPlatformVersion() { + Bundle b = Platform.getBundle(ECLIPSE_PLATFORM_PLUGIN_ID); + return b == null ? Version.emptyVersion : b.getVersion(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java new file mode 100644 index 00000000..278ad4f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.base.resources; + +import org.eclipse.swt.graphics.Image; + +/** + * Interface for Factory to generate icons for Android Editors + * @author Andrew Bowley + * + */ +public interface IEditorIconFactory { + + /** + * Returns an Image for a given icon name. + *

+ * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exist, a default icon will be + * generated automatically based on the name. + * @param color The color of the text in the automatically generated icons + */ + Image getColorIcon(String osName, int color); +} diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java new file mode 100644 index 00000000..c1b337a7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.base.resources; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +public interface ImageFactory { + /** + * Produces an edited version of given image + * {@link ImageFactory#getImageByName(String, String, ImageEditor)}. + */ + public interface ImageEditor { + /** + * The editor implementation needs to create an image data object based on a given image, or
+ * if no modification is necessary, return null.
+ *

+ * + * @param source A non-null source image. + * @return {@link ImageData} object, which can be null if no change required + */ + @NonNull public ImageData edit(@NonNull Image source); + } + + /** + * Produces an replacement version of given image + * {@link ImageFactory#getImageByName(String, String, Filter)}. + */ + public interface ReplacementImager { + /** + * The editor implementation needs to create an image data object based on a given image, or
+ * if no modification is necessary, return null.
+ *

+ * + * @param source A non-null source image. + * @return {@link ImageData} object, which can be null if no change required + */ + @NonNull public ImageData create(); + } + + /** + * Loads an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * The image is cached. Successive calls will return the same object.
+ * + * @param imageName The filename (with extension) of the image to load. + * @return {@link Image} object or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName); + + /** + * Returns an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * @param imageName The filename (with extension) of the image to load. + * @return {@link ImageDescriptor} object or null if the image file is not found. + */ + @Nullable + ImageDescriptor getDescriptorByName(String imageName); + + /** + * Loads an image given its filename (with its extension), caches it using the given + * {@code KeyName} name and applies a filter to it. + * Might return null if the image cannot be loaded. + * The image is cached. Successive calls using {@code KeyName} will return the same + * object directly (the filter is not re-applied in this case.)
+ *

+ * @param imageName Filename (with extension) of the image to load. + * @param keyName Image key reference + * @param imageEditor Image editor + * @return {@link Image} or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName,String keyName, ImageEditor imageEditor); + + /** + * Loads an image given its filename (with its extension) and if not found, + * uses supplied {@code ReplacementImager} to create a replacement. + * Might return null if the image cannot be loaded. + * The image is cached. Successive calls using {@code KeyName} will return the same + * object directly
+ *

+ * @param imageName Filename (with extension) of the image to load. + * @param keyName Image key reference + * @param imageEditor Image editor + * @return {@link Image} or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName, ReplacementImager replacementImager); + + /** + * Returns image for given image file path + * @param imagePath A valid file system path relative to the bundle location eg. "icons/smile.gif" + * @return {@link Image} object or null if the image file is not found. The caller must NOT dispose the image + */ + Image getImage(String imagePath); + + /** + * Dispose all image resources + */ + void dispose(); + +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java new file mode 100644 index 00000000..3c13a689 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.base.resources; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.jface.resource.ResourceManager; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.utils.ILogger; + +public class JFaceImageLoader implements ImageFactory { + + /** Image file location when using {@link #getImageByName(String)} */ + public static String ICONS_PATH = "icons/"; + + protected Bundle bundle; + protected PluginResourceProvider provider; + protected final Map filterMap = new HashMap<>(); + protected ResourceManager resourceManager; + protected ILogger logger; + + /** + * Loads bundle images + * Images are loaded using a path relative to the bundle location. + * + * Instances are mangaged by a JFace resource manager, and thus should never be disposed by the image consumer. + * + */ + + /** + * Construct an ImageLoader object using given UI plugin instance. + * This object provides imageDescriptorFromPlugin() method + * @param bundle + */ + public JFaceImageLoader(@NonNull PluginResourceProvider provider) + { + this.provider = provider; + createResourceManager(); + } + + /** + * Construct an ImageLoader object using given bundle instance + * @param bundle + */ + public JFaceImageLoader(@NonNull Bundle bundle) + { + this.bundle = bundle; + createResourceManager(); + } + + /** + * Construct an ImageLoader object using given class of plugin associated with the bundle + * @param bundleClass + */ + public JFaceImageLoader(Class bundleClass) + { + this(FrameworkUtil.getBundle(bundleClass)); + } + + public void setLogger(ILogger logger) { + this.logger = logger; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImageByName(java.lang.String) + */ + @Override + @Nullable + public Image getImageByName(String imageName) { + return getImage(ICONS_PATH + imageName); + } + + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImageByName(java.lang.String, java.lang.String, org.eclipse.andmore.base.resources.JFaceImageLoader.ImageEditor) + */ + @Override + @Nullable + public Image getImageByName(String imageName, + String keyName, + ImageEditor imageEditor) { + if (imageEditor == null) // No imageEditor means just load image. The keyName is irrelevant. + return getImageByName(imageName); + String imagePath = ICONS_PATH + imageName; + Image image = null; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor != null) { + ImageDescriptor imagefilterDescriptor = filterMap.get(keyName); + if (imagefilterDescriptor == null) { + // Assume filter input = output + imagefilterDescriptor = imageDescriptor; + image = resourceManager.createImage(imageDescriptor); + ImageData imageData = imageEditor.edit(image); + if (imageData != null) { + // Create new image from data + imagefilterDescriptor = ImageDescriptor.createFromImageData(imageData); + image = resourceManager.createImage(imagefilterDescriptor); + } + filterMap.put(keyName, imagefilterDescriptor); + } + else + image = resourceManager.createImage(imagefilterDescriptor); + } + return image; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImage(java.lang.String) + */ + @Override + public Image getImage(String imagePath) { + Image image = null; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor != null) { + image = resourceManager.createImage(imageDescriptor); + if ((image == null) && (logger != null)) + logger.error(null, "Image creation failed for image path " + imagePath); + } + return image; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#dispose() + */ + @Override + public void dispose() { + // Garbage collect system resources + if (resourceManager != null) + { + resourceManager.dispose(); + resourceManager = null; + } + } + + @Override + @Nullable + public Image getImageByName(String imageName, ReplacementImager replacementImager) { + String imagePath = ICONS_PATH + imageName; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor == null) { + ImageDescriptor replacementImageDescriptor = filterMap.get(imagePath); + if (replacementImageDescriptor == null) { + replacementImageDescriptor = ImageDescriptor.createFromImageData(replacementImager.create()); + filterMap.put(imagePath, replacementImageDescriptor); + } + return resourceManager.createImage(replacementImageDescriptor); + } + return resourceManager.createImage(imageDescriptor); + } + + @Override + @Nullable + public ImageDescriptor getDescriptorByName(String imageName) { + String imagePath = ICONS_PATH + imageName; + return descriptorFromPath(imagePath); + } + + protected ImageDescriptor descriptorFromPath(String imagePath) { + if (provider != null) { + ImageDescriptor descriptor = provider.descriptorFromPath(imagePath); + if ((logger != null) && (descriptor == null)) + logger.error(null, "Image descriptor null for image path: " +imagePath); + return descriptor; + } + ImageDescriptor imageDescriptor = null; + // An image descriptor is an object that knows how to create an SWT image. + URL url = FileLocator.find(bundle, new Path(imagePath), null); + if (url != null) { + imageDescriptor = ImageDescriptor.createFromURL(url); + if (logger != null) { + if (imageDescriptor != null) + logger.info("Image file found at " + url.toString()); + else + logger.error(null, "Image descriptor null for URL: " + url.toString()); + } + } + else if (logger != null) + logger.error(null, "Image path not found: " + imagePath); + return imageDescriptor; + } + + /** + * Returns local Resource Manager + * @return ResourceManager object + */ + protected void createResourceManager() { + if (resourceManager == null) + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() + { + // getResources() returns the ResourceManager for the current display. + // May only be called from a UI thread. + resourceManager = new LocalResourceManager(JFaceResources.getResources()); + } + }); + } + + + +} diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java new file mode 100644 index 00000000..9ca86fb4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.base.resources; + +import org.eclipse.jface.resource.ImageDescriptor; + +public interface PluginResourceProvider { + ImageDescriptor descriptorFromPath(String imagePath); +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath b/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore b/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.project b/andmore-swt/org.eclipse.andmore.swtmenubar/.project new file mode 100644 index 00000000..4b23edae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.swtmenbar + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF new file mode 100644 index 00000000..e1994f0e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.swtmenubar;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2" +Export-Package: com.android.menubar +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/MODULE_LICENSE_EPL b/andmore-swt/org.eclipse.andmore.swtmenubar/MODULE_LICENSE_EPL new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE b/andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE new file mode 100644 index 00000000..e06fed78 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE @@ -0,0 +1,224 @@ +*Eclipse Public License - v 1.0* + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF +THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +*1. DEFINITIONS* + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and +documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and +are distributed by that particular Contributor. A Contribution +'originates' from a Contributor if it was added to the Program by such +Contributor itself or anyone acting on such Contributor's behalf. +Contributions do not include additions to the Program which: (i) are +separate modules of software distributed in conjunction with the Program +under their own license agreement, and (ii) are not derivative works of +the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +*2. GRANT OF RIGHTS* + +a) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free copyright +license to reproduce, prepare derivative works of, publicly display, +publicly perform, distribute and sublicense the Contribution of such +Contributor, if any, and such derivative works, in source code and +object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby +grants Recipient a non-exclusive, worldwide, royalty-free patent license +under Licensed Patents to make, use, sell, offer to sell, import and +otherwise transfer the Contribution of such Contributor, if any, in +source code and object code form. This patent license shall apply to the +combination of the Contribution and the Program if, at the time the +Contribution is added by the Contributor, such addition of the +Contribution causes such combination to be covered by the Licensed +Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + +c) Recipient understands that although each Contributor grants the +licenses to its Contributions set forth herein, no assurances are +provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow Recipient +to distribute the Program, it is Recipient's responsibility to acquire +that license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright +license set forth in this Agreement. + +*3. REQUIREMENTS* + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties +and conditions, express and implied, including warranties or conditions +of title and non-infringement, and implied warranties or conditions of +merchantability and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and +consequential damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are +offered by that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable +manner on or through a medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained +within the Program. + +Each Contributor must identify itself as the originator of its +Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution. + +*4. COMMERCIAL DISTRIBUTION* + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor ("Commercial +Contributor") hereby agrees to defend and indemnify every other +Contributor ("Indemnified Contributor") against any losses, damages and +costs (collectively "Losses") arising from claims, lawsuits and other +legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the +Program in a commercial product offering. The obligations in this +section do not apply to any claims or Losses relating to any actual or +alleged intellectual property infringement. In order to qualify, an +Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial +Contributor to control, and cooperate with the Commercial Contributor +in, the defense and any related settlement negotiations. The Indemnified +Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages. + +*5. NO WARRANTY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED +ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES +OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR +A PARTICULAR PURPOSE. Each Recipient is solely responsible for +determining the appropriateness of using and distributing the Program +and assumes all risks associated with its exercise of rights under this +Agreement , including but not limited to the risks and costs of program +errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +*6. DISCLAIMER OF LIABILITY* + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +*7. GENERAL* + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and may +only be modified in the following manner. The Agreement Steward reserves +the right to publish new versions (including revisions) of this +Agreement from time to time. No one other than the Agreement Steward has +the right to modify this Agreement. The Eclipse Foundation is the +initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is +published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. Except as expressly stated in +Sections 2(a) and 2(b) above, Recipient receives no rights or licenses +to the intellectual property of any Contributor under this Agreement, +whether expressly, by implication, estoppel or otherwise. All rights in +the Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to +this Agreement will bring a legal action under this Agreement more than +one year after the cause of action arose. Each party waives its rights +to a jury trial in any resulting litigation. + + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/README b/andmore-swt/org.eclipse.andmore.swtmenubar/README new file mode 100644 index 00000000..56f3a619 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/README @@ -0,0 +1,80 @@ +Using the Eclipse project SwtMenuBar +------------------------------------ + +This project provides a platform-specific way to hook into +the default OS menu bar. + +On MacOS, it allows an SWT app to have an About menu item +and to hook into the default Preferences menu item. + +On Windows and Linux, an SWT Menu should be provided (typically +named "Tools") into which the About and Options menu items +will be added. + + +Consequently the implementation contains platform-specific source +folders for the Java files that rely on a platform-specific version +of SWT.jar. + +Right now we have the following source folders: +- src/ - Generic implementation for all platforms. +- src-darwin/ - Implementation for MacOS Carbon. + +*Only* the default "src/" folder is declared in the project .classpath +so that the project can be opened in Eclipse on any platform and still +work. However that means that on MacOS the custom src-darwin folder is +not used by default. + + + +1- To build the library: + +Do not use Eclipse to build the library. Instead use the makefile: + +$ cd $TOP_OF_ANDROID_TREE +$ . build/envsetup.sh && lunch sdk-eng +$ make swtmenubar + +This will create a Jar in /out/host//framework/ +that can then be included in the target application. + + +2- To use the library in a target application: + +Build the swtmenubar library as explained in step 1. + +In the target application, define a classpath variable in Eclipse: +- Open Preferences > Java > Build Path > Classpath Variables +- Create a new classpath variable named ANDROID_OUT_FRAMEWORK +- Set its folder value to /out/host//framework + +Then add a variable to the Build Path of the target project: +- Open Project > Properties > Java Build Path +- Select the "Libraries" tab +- Use "Add Variable" +- Select ANDROID_OUT_FRAMEWORK +- Select "Extend..." +- Select swtmenubar.jar (which you previously built at step 1) + + +3- Tip for developing this library: + +Keep in mind that src-darwin folder must not be added to the +source folder list, otherwise the library would not compile +on Windows or Linux. + +If you change anything to IMenuBarCallback, make sure to test +on a Mac to be sure you're not breaking the API. + +To work on this on a Mac, you can either: +a- simply temporarily add src-darwin as a source folder to the + build path and remove it before submitting. +b- or directly edit the java files and rebuild the library using + 'make swtmenubar' from a shell. + +To test the library, use 'make swtmenubar'. This will build the +library in out/... and the sdkmanager project is already setup +to find it there. + +-- +EOF diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle b/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle new file mode 100644 index 00000000..1870b695 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle @@ -0,0 +1,16 @@ +group = 'com.android.tools' +archivesBaseName = 'swtmenubar' + +dependencies { + compile project(':base:sdklib') + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + + main.java.srcDir 'src/main/java' + // Also add the MacOS specific sources for SWT Cocoa + main.java.srcDir 'src/main-darwin/java' +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties b/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties b/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties new file mode 100644 index 00000000..7b27c3e4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.swtmenubar +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Menubar +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml b/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml new file mode 100644 index 00000000..112d2cd7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.swtmenubar + eclipse-plugin + swtmenubar + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java new file mode 100644 index 00000000..ee449e6a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * History: + * Original code by the CarbonUIEnhancer from Agynami + * with the implementation being modified from the org.eclipse.ui.internal.cocoa.CocoaUIEnhancer, + * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection + * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project. + */ + +package com.android.menubar.internal; + +import com.android.menubar.IMenuBarCallback; +import com.android.menubar.IMenuBarEnhancer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.internal.C; +import org.eclipse.swt.internal.Callback; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class MenuBarEnhancerCocoa implements IMenuBarEnhancer { + + private static final long kAboutMenuItem = 0; + private static final long kPreferencesMenuItem = 2; + // private static final long kServicesMenuItem = 4; + // private static final long kHideApplicationMenuItem = 6; + private static final long kQuitMenuItem = 10; + + static long mSelPreferencesMenuItemSelected; + static long mSelAboutMenuItemSelected; + static Callback mProc3Args; + + private String mAppName; + + /** + * Class invoked via the Callback object to run the about and preferences + * actions. + *

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

+ */ + private static class ActionProctarget { + private final IMenuBarCallback mCallbacks; + + public ActionProctarget(IMenuBarCallback callbacks) { + mCallbacks = callbacks; + } + + /** + * Will be called on 32bit SWT. + */ + @SuppressWarnings("unused") + public int actionProc(int id, int sel, int arg0) { + return (int) actionProc((long) id, (long) sel, (long) arg0); + } + + /** + * Will be called on 64bit SWT. + */ + public long actionProc(long id, long sel, long arg0) { + if (sel == mSelAboutMenuItemSelected) { + mCallbacks.onAboutMenuSelected(); + } else if (sel == mSelPreferencesMenuItemSelected) { + mCallbacks.onPreferencesMenuSelected(); + } else { + // Unknown selection! + } + // Return value is not used. + return 0; + } + } + + /** + * Construct a new CocoaUIEnhancer. + * + * @param mAppName The name of the application. It will be used to customize + * the About and Quit menu items. If you do not wish to customize + * the About and Quit menu items, just pass null here. + */ + public MenuBarEnhancerCocoa() { + } + + public MenuBarMode getMenuBarMode() { + return MenuBarMode.MAC_OS; + } + + /** + * Setup the About and Preferences native menut items with the + * given application name and links them to the callback. + * + * @param appName The application name. + * @param display The SWT display. Must not be null. + * @param callbacks The callbacks invoked by the menus. + */ + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks) { + + mAppName = appName; + + // This is our callback object whose 'actionProc' method will be called + // when the About or Preferences menuItem is invoked. + ActionProctarget target = new ActionProctarget(callbacks); + + try { + // Initialize the menuItems. + initialize(target); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + // Schedule disposal of callback object + display.disposeExec(new Runnable() { + public void run() { + invoke(mProc3Args, "dispose"); + } + }); + } + + private void initialize(Object callbackObject) + throws Exception { + + Class osCls = classForName("org.eclipse.swt.internal.cocoa.OS"); + + // Register names in objective-c. + if (mSelAboutMenuItemSelected == 0) { + mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$ + mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:"); //$NON-NLS-1$ + } + + // Create an SWT Callback object that will invoke the actionProc method + // of our internal callback Object. + mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$ + Method getAddress = Callback.class.getMethod("getAddress", new Class[0]); + Object object = getAddress.invoke(mProc3Args, (Object[]) null); + long proc3 = convertToLong(object); + if (proc3 == 0) { + SWT.error(SWT.ERROR_NO_MORE_CALLBACKS); + } + + Class nsMenuCls = classForName("org.eclipse.swt.internal.cocoa.NSMenu"); + Class nsMenuitemCls = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"); + Class nsStringCls = classForName("org.eclipse.swt.internal.cocoa.NSString"); + Class nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication"); + + // Instead of creating a new delegate class in objective-c, + // just use the current SWTApplicationDelegate. An instance of this + // is a field of the Cocoa Display object and is already the target + // for the menuItems. So just get this class and add the new methods + // to it. + object = invoke(osCls, "objc_lookUpClass", new Object[] { + "SWTApplicationDelegate" + }); + long cls = convertToLong(object); + + // Add the action callbacks for Preferences and About menu items. + invoke(osCls, "class_addMethod", + new Object[] { + wrapPointer(cls), + wrapPointer(mSelPreferencesMenuItemSelected), + wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ + invoke(osCls, "class_addMethod", + new Object[] { + wrapPointer(cls), + wrapPointer(mSelAboutMenuItemSelected), + wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ + + // Get the Mac OS X Application menu. + Object sharedApplication = invoke(nsApplicationCls, "sharedApplication"); + Object mainMenu = invoke(sharedApplication, "mainMenu"); + Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] { + wrapPointer(0) + }); + Object appMenu = invoke(mainMenuItem, "submenu"); + + // Create the About menu command + Object aboutMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kAboutMenuItem) + }); + if (mAppName != null) { + Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { + "About " + mAppName + }); + invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] { + nsStr + }); + } + // Rename the quit action. + if (mAppName != null) { + Object quitMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kQuitMenuItem) + }); + Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { + "Quit " + mAppName + }); + invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] { + nsStr + }); + } + + // Enable the Preferences menuItem. + Object prefMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kPreferencesMenuItem) + }); + invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] { + true + }); + + // Set the action to execute when the About or Preferences menuItem is + // invoked. + // + // We don't need to set the target here as the current target is the + // SWTApplicationDelegate and we have registered the new selectors on + // it. So just set the new action to invoke the selector. + invoke(nsMenuitemCls, prefMenuItem, "setAction", + new Object[] { + wrapPointer(mSelPreferencesMenuItemSelected) + }); + invoke(nsMenuitemCls, aboutMenuItem, "setAction", + new Object[] { + wrapPointer(mSelAboutMenuItemSelected) + }); + } + + private long registerName(Class osCls, String name) + throws IllegalArgumentException, SecurityException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + Object object = invoke(osCls, "sel_registerName", new Object[] { + name + }); + return convertToLong(object); + } + + private long convertToLong(Object object) { + if (object instanceof Integer) { + Integer i = (Integer) object; + return i.longValue(); + } + if (object instanceof Long) { + Long l = (Long) object; + return l.longValue(); + } + return 0; + } + + private static Object wrapPointer(long value) { + Class PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class; + if (PTR_CLASS == long.class) { + return new Long(value); + } else { + return new Integer((int) value); + } + } + + private static Object invoke(Class clazz, String methodName, Object[] args) { + return invoke(clazz, null, methodName, args); + } + + private static Object invoke(Class clazz, Object target, String methodName, Object[] args) { + try { + Class[] signature = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + Class thisClass = args[i].getClass(); + if (thisClass == Integer.class) + signature[i] = int.class; + else if (thisClass == Long.class) + signature[i] = long.class; + else if (thisClass == Byte.class) + signature[i] = byte.class; + else if (thisClass == Boolean.class) + signature[i] = boolean.class; + else + signature[i] = thisClass; + } + Method method = clazz.getMethod(methodName, signature); + return method.invoke(target, args); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Class classForName(String classname) { + try { + Class cls = Class.forName(classname); + return cls; + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + private Object invoke(Class cls, String methodName) { + return invoke(cls, methodName, (Class[]) null, (Object[]) null); + } + + private Object invoke(Class cls, String methodName, Class[] paramTypes, + Object... arguments) { + try { + Method m = cls.getDeclaredMethod(methodName, paramTypes); + return m.invoke(null, arguments); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Object invoke(Object obj, String methodName) { + return invoke(obj, methodName, (Class[]) null, (Object[]) null); + } + + private Object invoke(Object obj, String methodName, Class[] paramTypes, Object... arguments) { + try { + Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes); + return m.invoke(obj, arguments); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java new file mode 100644 index 00000000..490d60fc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.menubar; + + + +/** + * Callbacks used by {@link IMenuBarEnhancer}. + */ +public interface IMenuBarCallback { + /** + * Invoked when the About menu item is selected by the user. + */ + abstract public void onAboutMenuSelected(); + + /** + * Invoked when the Preferences or Options menu item is selected by the user. + */ + abstract public void onPreferencesMenuSelected(); + + /** + * Used by the enhancer implementations to report errors. + * + * @param format A printf-like format string. + * @param args The parameters for the printf-like format string. + */ + abstract public void printError(String format, Object...args); +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java new file mode 100644 index 00000000..77b7051c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.menubar; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; + + +/** + * Interface to the platform-specific MenuBarEnhancer implementation returned by + * {@link MenuBarEnhancer#setupMenu}. + */ +public interface IMenuBarEnhancer { + + /** Values that indicate how the menu bar is being handlded. */ + public enum MenuBarMode { + /** + * The Mac-specific About and Preferences are being used. + * No File > Exit menu should be provided by the application. + */ + MAC_OS, + /** + * The provided SWT {@link Menu} is being used for About and Options. + * The application should provide a File > Exit menu. + */ + GENERIC + } + + /** + * Returns a {@link MenuBarMode} enum that indicates how the menu bar is going to + * or has been modified. This is implementation specific and can be called before or + * after {@link #setupMenu}. + *

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

+ * Users should not call this directly. + * {@link MenuBarEnhancer#setupMenu} should be used instead. + * + * @param appName Name used for the About menu item and similar. Must not be null. + * @param display The SWT display. Must not be null. + * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. + * Must not be null. + */ + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks); +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java new file mode 100644 index 00000000..179419c0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.menubar; + +import com.android.menubar.IMenuBarEnhancer.MenuBarMode; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; + + +/** + * On Mac, {@link MenuBarEnhancer#setupMenu} plugs a listener on the About and the + * Preferences menu items of the standard "application" menu in the menu bar. + * On Windows or Linux, it adds relevant items to a given {@link Menu} linked to + * the same listeners. + */ +public final class MenuBarEnhancer { + + private MenuBarEnhancer() { + } + + /** + * Creates an instance of {@link IMenuBarEnhancer} specific to the current platform + * and invoke its {@link IMenuBarEnhancer#setupMenu} to updates the menu bar. + *

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

+ * Potential errors are reported through {@link IMenuBarCallback}. + * + * @param appName Name used for the About menu item and similar. Must not be null. + * @param swtMenu For non-mac platform this is the menu where the "About" and + * the "Options" menu items are created. Typically the menu might be + * called "Tools". Must not be null. + * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. + * Must not be null. + * @return An actual {@link IMenuBarEnhancer} implementation. Can be null on failure. + * This is currently not of any use for the caller but is left in case + * we want to expand the functionality later. + */ + public static IMenuBarEnhancer setupMenu( + String appName, + final Menu swtMenu, + IMenuBarCallback callbacks) { + + IMenuBarEnhancer enhancer = getEnhancer(callbacks, swtMenu.getDisplay()); + + // Default implementation for generic platforms + if (enhancer == null) { + enhancer = getGenericEnhancer(swtMenu); + } + + try { + enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); + } catch (Exception e) { + // If the enhancer failed, try to fall back on the generic one + if (enhancer.getMenuBarMode() != MenuBarMode.GENERIC) { + enhancer = getGenericEnhancer(swtMenu); + try { + enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); + } catch (Exception e2) { + callbacks.printError("SWTMenuBar failed: %s", e2.toString()); + return null; + } + } + } + return enhancer; + } + + private static IMenuBarEnhancer getGenericEnhancer(final Menu swtMenu) { + IMenuBarEnhancer enhancer; + enhancer = new IMenuBarEnhancer() { + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.GENERIC; + } + + @Override + public void setupMenu( + String appName, + Display display, + final IMenuBarCallback callbacks) { + if (swtMenu.getItemCount() > 0) { + new MenuItem(swtMenu, SWT.SEPARATOR); + } + + // Note: we use "Preferences" on Mac and "Options" on Windows/Linux. + final MenuItem pref = new MenuItem(swtMenu, SWT.NONE); + pref.setText("&Options..."); + + final MenuItem about = new MenuItem(swtMenu, SWT.NONE); + about.setText("&About..."); + + pref.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + try { + pref.setEnabled(false); + callbacks.onPreferencesMenuSelected(); + super.widgetSelected(e); + } finally { + pref.setEnabled(true); + } + } + }); + + about.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + try { + about.setEnabled(false); + callbacks.onAboutMenuSelected(); + super.widgetSelected(e); + } finally { + about.setEnabled(true); + } + } + }); + } + }; + return enhancer; + } + + + public static IMenuBarEnhancer setupMenuManager( + String appName, + Display display, + final IMenuManager menuManager, + final IAction aboutAction, + final IAction preferencesAction, + final IAction quitAction) { + + IMenuBarCallback callbacks = new IMenuBarCallback() { + @Override + public void printError(String format, Object... args) { + System.err.println(String.format(format, args)); + } + + @Override + public void onPreferencesMenuSelected() { + if (preferencesAction != null) { + preferencesAction.run(); + } + } + + @Override + public void onAboutMenuSelected() { + if (aboutAction != null) { + aboutAction.run(); + } + } + }; + + IMenuBarEnhancer enhancer = getEnhancer(callbacks, display); + + // Default implementation for generic platforms + if (enhancer == null) { + enhancer = new IMenuBarEnhancer() { + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.GENERIC; + } + + @Override + public void setupMenu( + String appName, + Display display, + final IMenuBarCallback callbacks) { + if (!menuManager.isEmpty()) { + menuManager.add(new Separator()); + } + + if (aboutAction != null) { + menuManager.add(aboutAction); + } + if (preferencesAction != null) { + menuManager.add(preferencesAction); + } + if (quitAction != null) { + if (aboutAction != null || preferencesAction != null) { + menuManager.add(new Separator()); + } + menuManager.add(quitAction); + } + } + }; + } + + enhancer.setupMenu(appName, display, callbacks); + return enhancer; + } + + private static IMenuBarEnhancer getEnhancer(IMenuBarCallback callbacks, Display display) { + IMenuBarEnhancer enhancer = null; + String p = SWT.getPlatform(); + String className = null; + if ("cocoa".equals(p)) { //$NON-NLS-1$ + className = "com.android.menubar.internal.MenuBarEnhancerCocoa"; //$NON-NLS-1$ + + if (SWT.getVersion() >= 3700 && MenuBarEnhancer37.isSupported(display)) { + className = MenuBarEnhancer37.class.getName(); + } + } + + if (System.getenv("DEBUG_SWTMENUBAR") != null) { + callbacks.printError("DEBUG SwtMenuBar: SWT=%1$s, class=%2$s", p, className); + } + + if (className != null) { + try { + Class clazz = Class.forName(className); + enhancer = (IMenuBarEnhancer) clazz.newInstance(); + } catch (Exception e) { + // Log an error and fallback on the default implementation. + callbacks.printError( + "Failed to instantiate %1$s: %2$s", //$NON-NLS-1$ + className, + e.toString()); + } + } + return enhancer; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java new file mode 100644 index 00000000..3e03d6cc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * References: + * Based on the SWT snippet example at + * http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet354.java?view=co + */ + +package com.android.menubar; + + +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; + +import java.lang.reflect.Method; + +public class MenuBarEnhancer37 implements IMenuBarEnhancer { + + private static final int kAboutMenuItem = -1; // SWT.ID_ABOUT in SWT 3.7 + private static final int kPreferencesMenuItem = -2; // SWT.ID_PREFERENCES in SWT 3.7 + private static final int kQuitMenuItem = -6; // SWT.ID_QUIT in SWT 3.7 + + public MenuBarEnhancer37() { + } + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.MAC_OS; + } + + /** + * Setup the About and Preferences native menut items with the + * given application name and links them to the callback. + * + * @param appName The application name. + * @param display The SWT display. Must not be null. + * @param callbacks The callbacks invoked by the menus. + */ + @Override + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks) { + + try { + // Initialize the menuItems. + initialize(display, appName, callbacks); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + // Schedule disposal of callback object + display.disposeExec(new Runnable() { + @Override + public void run() { + } + }); + } + + /** + * Checks whether the required SWT 3.7 APIs are available. + *
+ * Calling this will load the class, which is OK since this class doesn't + * directly use any SWT 3.7 API -- instead it uses reflection so that the + * code can be loaded under SWT 3.6. + * + * @param display The current SWT display. + * @return True if the SWT 3.7 API are available and this enhancer can be used. + */ + public static boolean isSupported(Display display) { + try { + Object sysMenu = call0(display, "getSystemMenu"); + if (sysMenu instanceof Menu) { + return findMenuById((Menu)sysMenu, kPreferencesMenuItem) != null && + findMenuById((Menu)sysMenu, kAboutMenuItem) != null; + } + } catch (Exception ignore) {} + return false; + } + + private void initialize( + Display display, + String appName, + final IMenuBarCallback callbacks) + throws Exception { + Object sysMenu = call0(display, "getSystemMenu"); + if (sysMenu instanceof Menu) { + MenuItem menu = findMenuById((Menu)sysMenu, kPreferencesMenuItem); + if (menu != null) { + menu.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + callbacks.onPreferencesMenuSelected(); + } + }); + } + + menu = findMenuById((Menu)sysMenu, kAboutMenuItem); + if (menu != null) { + menu.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + callbacks.onAboutMenuSelected(); + } + }); + menu.setText("About " + appName); + } + + menu = findMenuById((Menu)sysMenu, kQuitMenuItem); + if (menu != null) { + // We already support the "quit" operation, no need for an extra handler here. + menu.setText("Quit " + appName); + } + + } + } + + private static Object call0(Object obj, String method) { + try { + Method m = obj.getClass().getMethod(method, (Class[])null); + if (m != null) { + return m.invoke(obj, (Object[])null); + } + } catch (Exception ignore) {} + return null; + } + + private static MenuItem findMenuById(Menu menu, int id) { + MenuItem[] items = menu.getItems(); + for (int i = items.length - 1; i >= 0; i--) { + MenuItem item = items[i]; + Object menuId = call0(item, "getID"); + if (menuId instanceof Integer) { + if (((Integer) menuId).intValue() == id) { + return item; + } + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml b/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml new file mode 100644 index 00000000..22023bf7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath b/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore b/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.project b/andmore-swt/org.eclipse.andmore.traceviewuilib/.project new file mode 100644 index 00000000..dcc06edb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.traceviewuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..09e73fec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.traceviewuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.andmore.sdkstats +Export-Package: com.android.traceview +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/MODULE_LICENSE_EPL b/andmore-swt/org.eclipse.andmore.traceviewuilib/MODULE_LICENSE_EPL new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE b/andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/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/andmore-swt/org.eclipse.andmore.traceviewuilib/README b/andmore-swt/org.eclipse.andmore.traceviewuilib/README new file mode 100644 index 00000000..7f9f87dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for traceview. + +traceview requires SWT to compile. + +SWT is available in the depot under //device/prebuild//swt + +Because the build path cannot contain relative path that are not inside the project directory, +the .classpath file references a user library called ANDROID_SWT. + +In order to compile the project, make a user library called ANDROID_SWT containing the jar +available at //device/prebuild//swt. diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle new file mode 100644 index 00000000..1d923d7f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle @@ -0,0 +1,23 @@ +group = 'com.android.tools' +archivesBaseName = 'traceview' + +dependencies { + compile project(':base:common') + compile project(':swt:sdkstats') +} + +sdk { + linux { + item('etc/traceview') { executable true } + } + mac { + item('etc/traceview') { executable true } + } + windows { + item 'etc/traceview.bat' + } +} + +// configure the manifest of the buildDistributionJar task. +sdkJar.manifest.attributes("Main-Class": "com.android.traceview.MainWindow") + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview new file mode 100644 index 00000000..4bb5ff54 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview @@ -0,0 +1,108 @@ +#!/bin/bash +# +# Copyright 2005-2006, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +progname=`basename "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/"${progname}" +cd "${oldwd}" + +jarfile=traceview.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo "${progname}: can't find $jarfile" + exit 1 +fi + +javaCmd="java" + +os=`uname` +if [ $os == 'Darwin' ]; then + javaOpts="-Xmx1600M -XstartOnFirstThread" +else + javaOpts="-Xmx1600M" +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +while expr "x$1" : 'x-J' >/dev/null; do + opt=`expr "x$1" : 'x-J\(.*\)'` + javaOpts="${javaOpts} -${opt}" + shift +done + +jarpath="$frameworkdir/$jarfile" + +# Figure out the path to the swt.jar for the current architecture. +# if ANDROID_SWT is defined, then just use this. +# else, if running in the Android source tree, then look for the correct swt folder in prebuilt +# else, look for the correct swt folder in the SDK under tools/lib/ +swtpath="" +if [ -n "$ANDROID_SWT" ]; then + swtpath="$ANDROID_SWT" +else + vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` + if [ -n "$ANDROID_BUILD_TOP" ]; then + osname=`uname -s | tr A-Z a-z` + swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" + else + swtpath="${frameworkdir}/${vmarch}" + fi +fi + +# Combine the swtpath and the framework dir path. +if [ -d "$swtpath" ]; then + frameworkdir="${swtpath}:${frameworkdir}" +else + echo "SWT folder '${swtpath}' does not exist." + echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." + exit 1 +fi + +if [ -x $progdir/monitor ]; then + echo "The standalone version of traceview is deprecated." + echo "Please use Android Device Monitor (tools/monitor) instead." +fi +exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.traceview.toolsdir="$progdir" -jar "$jarpath" "$@" diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat new file mode 100644 index 00000000..92fd8bef --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat @@ -0,0 +1,65 @@ +@echo off +rem Copyright (C) 2007 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +rem Check we have a valid Java.exe in the path. +set java_exe= +call lib\find_java.bat +if not defined java_exe goto :EOF + +set jarfile=traceview.jar +set frameworkdir=. + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=lib + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=..\framework + +:JarFileOk + +set jarpath=%frameworkdir%\%jarfile% + +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a + +:SwtDone + +if exist "%swt_path%" goto SetPath + echo SWT folder '%swt_path%' does not exist. + echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. + exit /B + +:SetPath +set javaextdirs=%swt_path%;%frameworkdir% + +echo The standalone version of traceview is deprecated. +echo Please use Android Device Monitor (tools/monitor) instead. +call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" -Dcom.android.traceview.toolsdir= -jar %jarpath% %* diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties b/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties new file mode 100644 index 00000000..53bfd0c9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.traceview +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Traceview +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml b/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml new file mode 100644 index 00000000..ece003fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.traceviewuilib + eclipse-plugin + traceviewuilib + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java new file mode 100644 index 00000000..3bc434ba --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.swt.graphics.Color; + +class Call implements TimeLineView.Block { + final private ThreadData mThreadData; + final private MethodData mMethodData; + final Call mCaller; // the caller, or null if this is the root + + private String mName; + private boolean mIsRecursive; + + long mGlobalStartTime; + long mGlobalEndTime; + + long mThreadStartTime; + long mThreadEndTime; + + long mInclusiveRealTime; // real time spent in this call including its children + long mExclusiveRealTime; // real time spent in this call including its children + + long mInclusiveCpuTime; // cpu time spent in this call including its children + long mExclusiveCpuTime; // cpu time spent in this call excluding its children + + Call(ThreadData threadData, MethodData methodData, Call caller) { + mThreadData = threadData; + mMethodData = methodData; + mName = methodData.getProfileName(); + mCaller = caller; + } + + public void updateName() { + mName = mMethodData.getProfileName(); + } + + @Override + public double addWeight(int x, int y, double weight) { + return mMethodData.addWeight(x, y, weight); + } + + @Override + public void clearWeight() { + mMethodData.clearWeight(); + } + + @Override + public long getStartTime() { + return mGlobalStartTime; + } + + @Override + public long getEndTime() { + return mGlobalEndTime; + } + + @Override + public long getExclusiveCpuTime() { + return mExclusiveCpuTime; + } + + @Override + public long getInclusiveCpuTime() { + return mInclusiveCpuTime; + } + + @Override + public long getExclusiveRealTime() { + return mExclusiveRealTime; + } + + @Override + public long getInclusiveRealTime() { + return mInclusiveRealTime; + } + + @Override + public Color getColor() { + return mMethodData.getColor(); + } + + @Override + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public ThreadData getThreadData() { + return mThreadData; + } + + public int getThreadId() { + return mThreadData.getId(); + } + + @Override + public MethodData getMethodData() { + return mMethodData; + } + + @Override + public boolean isContextSwitch() { + return mMethodData.getId() == -1; + } + + @Override + public boolean isIgnoredBlock() { + // Ignore the top-level call or context switches within the top-level call. + return mCaller == null || isContextSwitch() && mCaller.mCaller == null; + } + + @Override + public TimeLineView.Block getParentBlock() { + return mCaller; + } + + public boolean isRecursive() { + return mIsRecursive; + } + + void setRecursive(boolean isRecursive) { + mIsRecursive = isRecursive; + } + + void addCpuTime(long elapsedCpuTime) { + mExclusiveCpuTime += elapsedCpuTime; + mInclusiveCpuTime += elapsedCpuTime; + } + + /** + * Record time spent in the method call. + */ + void finish() { + if (mCaller != null) { + mCaller.mInclusiveCpuTime += mInclusiveCpuTime; + mCaller.mInclusiveRealTime += mInclusiveRealTime; + } + + mMethodData.addElapsedExclusive(mExclusiveCpuTime, mExclusiveRealTime); + if (!mIsRecursive) { + mMethodData.addTopExclusive(mExclusiveCpuTime, mExclusiveRealTime); + } + mMethodData.addElapsedInclusive(mInclusiveCpuTime, mInclusiveRealTime, + mIsRecursive, mCaller); + } + + public static final class TraceAction { + public static final int ACTION_ENTER = 0; + public static final int ACTION_EXIT = 1; + + public final int mAction; + public final Call mCall; + + public TraceAction(int action, Call call) { + mAction = action; + mCall = call; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java new file mode 100644 index 00000000..cdbc32fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.util.HashMap; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Display; + +public class ColorController { + private static final int[] systemColors = { SWT.COLOR_BLUE, SWT.COLOR_RED, + SWT.COLOR_GREEN, SWT.COLOR_CYAN, SWT.COLOR_MAGENTA, SWT.COLOR_DARK_BLUE, + SWT.COLOR_DARK_RED, SWT.COLOR_DARK_GREEN, SWT.COLOR_DARK_YELLOW, + SWT.COLOR_DARK_CYAN, SWT.COLOR_DARK_MAGENTA, SWT.COLOR_BLACK }; + + private static RGB[] rgbColors = { new RGB(90, 90, 255), // blue + new RGB(0, 240, 0), // green + new RGB(255, 0, 0), // red + new RGB(0, 255, 255), // cyan + new RGB(255, 80, 255), // magenta + new RGB(200, 200, 0), // yellow + new RGB(40, 0, 200), // dark blue + new RGB(150, 255, 150), // light green + new RGB(150, 0, 0), // dark red + new RGB(30, 150, 150), // dark cyan + new RGB(200, 200, 255), // light blue + new RGB(0, 120, 0), // dark green + new RGB(255, 150, 150), // light red + new RGB(140, 80, 140), // dark magenta + new RGB(150, 100, 50), // brown + new RGB(70, 70, 70), // dark grey + }; + + private static HashMap colorCache = new HashMap(); + private static HashMap imageCache = new HashMap(); + + public ColorController() { + } + + public static Color requestColor(Display display, RGB rgb) { + return requestColor(display, rgb.red, rgb.green, rgb.blue); + } + + public static Image requestColorSquare(Display display, RGB rgb) { + return requestColorSquare(display, rgb.red, rgb.green, rgb.blue); + } + + public static Color requestColor(Display display, int red, int green, int blue) { + int key = (red << 16) | (green << 8) | blue; + Color color = colorCache.get(key); + if (color == null) { + color = new Color(display, red, green, blue); + colorCache.put(key, color); + } + return color; + } + + public static Image requestColorSquare(Display display, int red, int green, int blue) { + int key = (red << 16) | (green << 8) | blue; + Image image = imageCache.get(key); + if (image == null) { + image = new Image(display, 8, 14); + GC gc = new GC(image); + Color color = requestColor(display, red, green, blue); + gc.setBackground(color); + gc.fillRectangle(image.getBounds()); + gc.dispose(); + imageCache.put(key, image); + } + return image; + } + + public static void assignMethodColors(Display display, MethodData[] methods) { + int nextColorIndex = 0; + for (MethodData md : methods) { + RGB rgb = rgbColors[nextColorIndex]; + if (++nextColorIndex == rgbColors.length) + nextColorIndex = 0; + Color color = requestColor(display, rgb); + Image image = requestColorSquare(display, rgb); + md.setColor(color); + md.setImage(image); + + // Compute and set a faded color + int fadedRed = 150 + rgb.red / 4; + int fadedGreen = 150 + rgb.green / 4; + int fadedBlue = 150 + rgb.blue / 4; + RGB faded = new RGB(fadedRed, fadedGreen, fadedBlue); + color = requestColor(display, faded); + image = requestColorSquare(display, faded); + md.setFadedColor(color); + md.setFadedImage(image); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java new file mode 100644 index 00000000..9fd586b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java @@ -0,0 +1,754 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.BufferUnderflowException; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DmTraceReader extends TraceReader { + private static final int TRACE_MAGIC = 0x574f4c53; + + private static final int METHOD_TRACE_ENTER = 0x00; // method entry + private static final int METHOD_TRACE_EXIT = 0x01; // method exit + private static final int METHOD_TRACE_UNROLL = 0x02; // method exited by exception unrolling + + // When in dual clock mode, we report that a context switch has occurred + // when skew between the real time and thread cpu clocks is more than this + // many microseconds. + private static final long MIN_CONTEXT_SWITCH_TIME_USEC = 100; + + private enum ClockSource { + THREAD_CPU, WALL, DUAL, + }; + + private int mVersionNumber; + private boolean mRegression; + private ProfileProvider mProfileProvider; + private String mTraceFileName; + private MethodData mTopLevel; + private ArrayList mCallList; + private HashMap mPropertiesMap; + private HashMap mMethodMap; + private HashMap mThreadMap; + private ThreadData[] mSortedThreads; + private MethodData[] mSortedMethods; + private long mTotalCpuTime; + private long mTotalRealTime; + private MethodData mContextSwitch; + private int mRecordSize; + private ClockSource mClockSource; + + // A regex for matching the thread "id name" lines in the .key file + private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)"); //$NON-NLS-1$ + + public DmTraceReader(String traceFileName, boolean regression) throws IOException { + mTraceFileName = traceFileName; + mRegression = regression; + mPropertiesMap = new HashMap(); + mMethodMap = new HashMap(); + mThreadMap = new HashMap(); + mCallList = new ArrayList(); + + // Create a single top-level MethodData object to hold the profile data + // for time spent in the unknown caller. + mTopLevel = new MethodData(0, "(toplevel)"); + mContextSwitch = new MethodData(-1, "(context switch)"); + mMethodMap.put(0, mTopLevel); + mMethodMap.put(-1, mContextSwitch); + generateTrees(); + } + + void generateTrees() throws IOException { + long offset = parseKeys(); + parseData(offset); + analyzeData(); + } + + @Override + public ProfileProvider getProfileProvider() { + if (mProfileProvider == null) + mProfileProvider = new ProfileProvider(this); + return mProfileProvider; + } + + private MappedByteBuffer mapFile(String filename, long offset) throws IOException { + MappedByteBuffer buffer = null; + FileInputStream dataFile = new FileInputStream(filename); + try { + File file = new File(filename); + FileChannel fc = dataFile.getChannel(); + buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + return buffer; + } finally { + dataFile.close(); // this *also* closes the associated channel, fc + } + } + + private void readDataFileHeader(MappedByteBuffer buffer) { + int magic = buffer.getInt(); + if (magic != TRACE_MAGIC) { + System.err.printf( + "Error: magic number mismatch; got 0x%x, expected 0x%x\n", + magic, TRACE_MAGIC); + throw new RuntimeException(); + } + + // read version + int version = buffer.getShort(); + if (version != mVersionNumber) { + System.err.printf( + "Error: version number mismatch; got %d in data header but %d in options\n", + version, mVersionNumber); + throw new RuntimeException(); + } + if (version < 1 || version > 3) { + System.err.printf( + "Error: unsupported trace version number %d. " + + "Please use a newer version of TraceView to read this file.", version); + throw new RuntimeException(); + } + + // read offset + int offsetToData = buffer.getShort() - 16; + + // read startWhen + buffer.getLong(); + + // read record size + if (version == 1) { + mRecordSize = 9; + } else if (version == 2) { + mRecordSize = 10; + } else { + mRecordSize = buffer.getShort(); + offsetToData -= 2; + } + + // Skip over offsetToData bytes + while (offsetToData-- > 0) { + buffer.get(); + } + } + + private void parseData(long offset) throws IOException { + MappedByteBuffer buffer = mapFile(mTraceFileName, offset); + readDataFileHeader(buffer); + + ArrayList trace = null; + if (mClockSource == ClockSource.THREAD_CPU) { + trace = new ArrayList(); + } + + final boolean haveThreadClock = mClockSource != ClockSource.WALL; + final boolean haveGlobalClock = mClockSource != ClockSource.THREAD_CPU; + + // Parse all call records to obtain elapsed time information. + ThreadData prevThreadData = null; + for (;;) { + int threadId; + int methodId; + long threadTime, globalTime; + try { + int recordSize = mRecordSize; + + if (mVersionNumber == 1) { + threadId = buffer.get(); + recordSize -= 1; + } else { + threadId = buffer.getShort(); + recordSize -= 2; + } + + methodId = buffer.getInt(); + recordSize -= 4; + + switch (mClockSource) { + case WALL: + threadTime = 0; + globalTime = buffer.getInt(); + recordSize -= 4; + break; + case DUAL: + threadTime = buffer.getInt(); + globalTime = buffer.getInt(); + recordSize -= 8; + break; + default: + case THREAD_CPU: + threadTime = buffer.getInt(); + globalTime = 0; + recordSize -= 4; + break; + } + + while (recordSize-- > 0) { + buffer.get(); + } + } catch (BufferUnderflowException ex) { + break; + } + + int methodAction = methodId & 0x03; + methodId = methodId & ~0x03; + MethodData methodData = mMethodMap.get(methodId); + if (methodData == null) { + String name = String.format("(0x%1$x)", methodId); //$NON-NLS-1$ + methodData = new MethodData(methodId, name); + mMethodMap.put(methodId, methodData); + } + + ThreadData threadData = mThreadMap.get(threadId); + if (threadData == null) { + String name = String.format("[%1$d]", threadId); //$NON-NLS-1$ + threadData = new ThreadData(threadId, name, mTopLevel); + mThreadMap.put(threadId, threadData); + } + + long elapsedGlobalTime = 0; + if (haveGlobalClock) { + if (!threadData.mHaveGlobalTime) { + threadData.mGlobalStartTime = globalTime; + threadData.mHaveGlobalTime = true; + } else { + elapsedGlobalTime = globalTime - threadData.mGlobalEndTime; + } + threadData.mGlobalEndTime = globalTime; + } + + if (haveThreadClock) { + long elapsedThreadTime = 0; + if (!threadData.mHaveThreadTime) { + threadData.mThreadStartTime = threadTime; + threadData.mThreadCurrentTime = threadTime; + threadData.mHaveThreadTime = true; + } else { + elapsedThreadTime = threadTime - threadData.mThreadEndTime; + } + threadData.mThreadEndTime = threadTime; + + if (!haveGlobalClock) { + // Detect context switches whenever execution appears to switch from one + // thread to another. This assumption is only valid on uniprocessor + // systems (which is why we now have a dual clock mode). + // We represent context switches in the trace by pushing a call record + // with MethodData mContextSwitch onto the stack of the previous + // thread. We arbitrarily set the start and end time of the context + // switch such that the context switch occurs in the middle of the thread + // time and itself accounts for zero thread time. + if (prevThreadData != null && prevThreadData != threadData) { + // Begin context switch from previous thread. + Call switchCall = prevThreadData.enter(mContextSwitch, trace); + switchCall.mThreadStartTime = prevThreadData.mThreadEndTime; + mCallList.add(switchCall); + + // Return from context switch to current thread. + Call top = threadData.top(); + if (top.getMethodData() == mContextSwitch) { + threadData.exit(mContextSwitch, trace); + long beforeSwitch = elapsedThreadTime / 2; + top.mThreadStartTime += beforeSwitch; + top.mThreadEndTime = top.mThreadStartTime; + } + } + prevThreadData = threadData; + } else { + // If we have a global clock, then we can detect context switches (or blocking + // calls or cpu suspensions or clock anomalies) by comparing global time to + // thread time for successive calls that occur on the same thread. + // As above, we represent the context switch using a special method call. + long sleepTime = elapsedGlobalTime - elapsedThreadTime; + if (sleepTime > MIN_CONTEXT_SWITCH_TIME_USEC) { + Call switchCall = threadData.enter(mContextSwitch, trace); + long beforeSwitch = elapsedThreadTime / 2; + long afterSwitch = elapsedThreadTime - beforeSwitch; + switchCall.mGlobalStartTime = globalTime - elapsedGlobalTime + beforeSwitch; + switchCall.mGlobalEndTime = globalTime - afterSwitch; + switchCall.mThreadStartTime = threadTime - afterSwitch; + switchCall.mThreadEndTime = switchCall.mThreadStartTime; + threadData.exit(mContextSwitch, trace); + mCallList.add(switchCall); + } + } + + // Add thread CPU time. + Call top = threadData.top(); + top.addCpuTime(elapsedThreadTime); + } + + switch (methodAction) { + case METHOD_TRACE_ENTER: { + Call call = threadData.enter(methodData, trace); + if (haveGlobalClock) { + call.mGlobalStartTime = globalTime; + } + if (haveThreadClock) { + call.mThreadStartTime = threadTime; + } + mCallList.add(call); + break; + } + case METHOD_TRACE_EXIT: + case METHOD_TRACE_UNROLL: { + Call call = threadData.exit(methodData, trace); + if (call != null) { + if (haveGlobalClock) { + call.mGlobalEndTime = globalTime; + } + if (haveThreadClock) { + call.mThreadEndTime = threadTime; + } + } + break; + } + default: + throw new RuntimeException("Unrecognized method action: " + methodAction); + } + } + + // Exit any pending open-ended calls. + for (ThreadData threadData : mThreadMap.values()) { + threadData.endTrace(trace); + } + + // Recreate the global timeline from thread times, if needed. + if (!haveGlobalClock) { + long globalTime = 0; + prevThreadData = null; + for (TraceAction traceAction : trace) { + Call call = traceAction.mCall; + ThreadData threadData = call.getThreadData(); + + if (traceAction.mAction == TraceAction.ACTION_ENTER) { + long threadTime = call.mThreadStartTime; + globalTime += call.mThreadStartTime - threadData.mThreadCurrentTime; + call.mGlobalStartTime = globalTime; + if (!threadData.mHaveGlobalTime) { + threadData.mHaveGlobalTime = true; + threadData.mGlobalStartTime = globalTime; + } + threadData.mThreadCurrentTime = threadTime; + } else if (traceAction.mAction == TraceAction.ACTION_EXIT) { + long threadTime = call.mThreadEndTime; + globalTime += call.mThreadEndTime - threadData.mThreadCurrentTime; + call.mGlobalEndTime = globalTime; + threadData.mGlobalEndTime = globalTime; + threadData.mThreadCurrentTime = threadTime; + } // else, ignore ACTION_INCOMPLETE calls, nothing to do + prevThreadData = threadData; + } + } + + // Finish updating all calls and calculate the total time spent. + for (int i = mCallList.size() - 1; i >= 0; i--) { + Call call = mCallList.get(i); + + // Calculate exclusive real-time by subtracting inclusive real time + // accumulated by children from the total span. + long realTime = call.mGlobalEndTime - call.mGlobalStartTime; + call.mExclusiveRealTime = Math.max(realTime - call.mInclusiveRealTime, 0); + call.mInclusiveRealTime = realTime; + + call.finish(); + } + mTotalCpuTime = 0; + mTotalRealTime = 0; + for (ThreadData threadData : mThreadMap.values()) { + Call rootCall = threadData.getRootCall(); + threadData.updateRootCallTimeBounds(); + rootCall.finish(); + mTotalCpuTime += rootCall.mInclusiveCpuTime; + mTotalRealTime += rootCall.mInclusiveRealTime; + } + + if (mRegression) { + System.out.format("totalCpuTime %dus\n", mTotalCpuTime); + System.out.format("totalRealTime %dus\n", mTotalRealTime); + + dumpThreadTimes(); + dumpCallTimes(); + } + } + + static final int PARSE_VERSION = 0; + static final int PARSE_THREADS = 1; + static final int PARSE_METHODS = 2; + static final int PARSE_OPTIONS = 4; + + long parseKeys() throws IOException { + long offset = 0; + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader( + new FileInputStream(mTraceFileName), "US-ASCII")); + + int mode = PARSE_VERSION; + String line = null; + while (true) { + line = in.readLine(); + if (line == null) { + throw new IOException("Key section does not have an *end marker"); + } + + // Calculate how much we have read from the file so far. The + // extra byte is for the line ending not included by readLine(). + offset += line.length() + 1; + if (line.startsWith("*")) { + if (line.equals("*version")) { + mode = PARSE_VERSION; + continue; + } + if (line.equals("*threads")) { + mode = PARSE_THREADS; + continue; + } + if (line.equals("*methods")) { + mode = PARSE_METHODS; + continue; + } + if (line.equals("*end")) { + break; + } + } + switch (mode) { + case PARSE_VERSION: + mVersionNumber = Integer.decode(line); + mode = PARSE_OPTIONS; + break; + case PARSE_THREADS: + parseThread(line); + break; + case PARSE_METHODS: + parseMethod(line); + break; + case PARSE_OPTIONS: + parseOption(line); + break; + } + } + } catch (FileNotFoundException ex) { + System.err.println(ex.getMessage()); + } finally { + if (in != null) { + in.close(); + } + } + + if (mClockSource == null) { + mClockSource = ClockSource.THREAD_CPU; + } + + return offset; + } + + void parseOption(String line) { + String[] tokens = line.split("="); + if (tokens.length == 2) { + String key = tokens[0]; + String value = tokens[1]; + mPropertiesMap.put(key, value); + + if (key.equals("clock")) { + if (value.equals("thread-cpu")) { + mClockSource = ClockSource.THREAD_CPU; + } else if (value.equals("wall")) { + mClockSource = ClockSource.WALL; + } else if (value.equals("dual")) { + mClockSource = ClockSource.DUAL; + } + } + } + } + + void parseThread(String line) { + String idStr = null; + String name = null; + Matcher matcher = mIdNamePattern.matcher(line); + if (matcher.find()) { + idStr = matcher.group(1); + name = matcher.group(2); + } + if (idStr == null) return; + if (name == null) name = "(unknown)"; + + int id = Integer.decode(idStr); + mThreadMap.put(id, new ThreadData(id, name, mTopLevel)); + } + + void parseMethod(String line) { + String[] tokens = line.split("\t"); + int id = Long.decode(tokens[0]).intValue(); + String className = tokens[1]; + String methodName = null; + String signature = null; + String pathname = null; + int lineNumber = -1; + if (tokens.length == 6) { + methodName = tokens[2]; + signature = tokens[3]; + pathname = tokens[4]; + lineNumber = Integer.decode(tokens[5]); + pathname = constructPathname(className, pathname); + } else if (tokens.length > 2) { + if (tokens[3].startsWith("(")) { + methodName = tokens[2]; + signature = tokens[3]; + } else { + pathname = tokens[2]; + lineNumber = Integer.decode(tokens[3]); + } + } + + mMethodMap.put(id, new MethodData(id, className, methodName, signature, + pathname, lineNumber)); + } + + private String constructPathname(String className, String pathname) { + int index = className.lastIndexOf('/'); + if (index > 0 && index < className.length() - 1 + && pathname.endsWith(".java")) + pathname = className.substring(0, index + 1) + pathname; + return pathname; + } + + private void analyzeData() { + final TimeBase timeBase = getPreferredTimeBase(); + + // Sort the threads into decreasing cpu time + Collection tv = mThreadMap.values(); + mSortedThreads = tv.toArray(new ThreadData[tv.size()]); + Arrays.sort(mSortedThreads, new Comparator() { + @Override + public int compare(ThreadData td1, ThreadData td2) { + if (timeBase.getTime(td2) > timeBase.getTime(td1)) + return 1; + if (timeBase.getTime(td2) < timeBase.getTime(td1)) + return -1; + return td2.getName().compareTo(td1.getName()); + } + }); + + // Sort the methods into decreasing inclusive time + Collection mv = mMethodMap.values(); + MethodData[] methods; + methods = mv.toArray(new MethodData[mv.size()]); + Arrays.sort(methods, new Comparator() { + @Override + public int compare(MethodData md1, MethodData md2) { + if (timeBase.getElapsedInclusiveTime(md2) > timeBase.getElapsedInclusiveTime(md1)) + return 1; + if (timeBase.getElapsedInclusiveTime(md2) < timeBase.getElapsedInclusiveTime(md1)) + return -1; + return md1.getName().compareTo(md2.getName()); + } + }); + + // Count the number of methods with non-zero inclusive time + int nonZero = 0; + for (MethodData md : methods) { + if (timeBase.getElapsedInclusiveTime(md) == 0) + break; + nonZero += 1; + } + + // Copy the methods with non-zero time + mSortedMethods = new MethodData[nonZero]; + int ii = 0; + for (MethodData md : methods) { + if (timeBase.getElapsedInclusiveTime(md) == 0) + break; + md.setRank(ii); + mSortedMethods[ii++] = md; + } + + // Let each method analyze its profile data + for (MethodData md : mSortedMethods) { + md.analyzeData(timeBase); + } + + // Update all the calls to include the method rank in + // their name. + for (Call call : mCallList) { + call.updateName(); + } + + if (mRegression) { + dumpMethodStats(); + } + } + + /* + * This method computes a list of records that describe the the execution + * timeline for each thread. Each record is a pair: (row, block) where: row: + * is the ThreadData object block: is the call (containing the start and end + * times) + */ + @Override + public ArrayList getThreadTimeRecords() { + TimeLineView.Record record; + ArrayList timeRecs; + timeRecs = new ArrayList(); + + // For each thread, push a "toplevel" call that encompasses the + // entire execution of the thread. + for (ThreadData threadData : mSortedThreads) { + if (!threadData.isEmpty() && threadData.getId() != 0) { + record = new TimeLineView.Record(threadData, threadData.getRootCall()); + timeRecs.add(record); + } + } + + for (Call call : mCallList) { + record = new TimeLineView.Record(call.getThreadData(), call); + timeRecs.add(record); + } + + if (mRegression) { + dumpTimeRecs(timeRecs); + System.exit(0); + } + return timeRecs; + } + + private void dumpThreadTimes() { + System.out.print("\nThread Times\n"); + System.out.print("id t-start t-end g-start g-end name\n"); + for (ThreadData threadData : mThreadMap.values()) { + System.out.format("%2d %8d %8d %8d %8d %s\n", + threadData.getId(), + threadData.mThreadStartTime, threadData.mThreadEndTime, + threadData.mGlobalStartTime, threadData.mGlobalEndTime, + threadData.getName()); + } + } + + private void dumpCallTimes() { + System.out.print("\nCall Times\n"); + System.out.print("id t-start t-end g-start g-end excl. incl. method\n"); + for (Call call : mCallList) { + System.out.format("%2d %8d %8d %8d %8d %8d %8d %s\n", + call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, + call.mGlobalStartTime, call.mGlobalEndTime, + call.mExclusiveCpuTime, call.mInclusiveCpuTime, + call.getMethodData().getName()); + } + } + + private void dumpMethodStats() { + System.out.print("\nMethod Stats\n"); + System.out.print("Excl Cpu Incl Cpu Excl Real Incl Real Calls Method\n"); + for (MethodData md : mSortedMethods) { + System.out.format("%9d %9d %9d %9d %9s %s\n", + md.getElapsedExclusiveCpuTime(), md.getElapsedInclusiveCpuTime(), + md.getElapsedExclusiveRealTime(), md.getElapsedInclusiveRealTime(), + md.getCalls(), md.getProfileName()); + } + } + + private void dumpTimeRecs(ArrayList timeRecs) { + System.out.print("\nTime Records\n"); + System.out.print("id t-start t-end g-start g-end method\n"); + for (TimeLineView.Record record : timeRecs) { + Call call = (Call) record.block; + System.out.format("%2d %8d %8d %8d %8d %s\n", + call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, + call.mGlobalStartTime, call.mGlobalEndTime, + call.getMethodData().getName()); + } + } + + @Override + public HashMap getThreadLabels() { + HashMap labels = new HashMap(); + for (ThreadData t : mThreadMap.values()) { + labels.put(t.getId(), t.getName()); + } + return labels; + } + + @Override + public MethodData[] getMethods() { + return mSortedMethods; + } + + @Override + public ThreadData[] getThreads() { + return mSortedThreads; + } + + @Override + public long getTotalCpuTime() { + return mTotalCpuTime; + } + + @Override + public long getTotalRealTime() { + return mTotalRealTime; + } + + @Override + public boolean haveCpuTime() { + return mClockSource != ClockSource.WALL; + } + + @Override + public boolean haveRealTime() { + return mClockSource != ClockSource.THREAD_CPU; + } + + @Override + public HashMap getProperties() { + return mPropertiesMap; + } + + @Override + public TimeBase getPreferredTimeBase() { + if (mClockSource == ClockSource.WALL) { + return TimeBase.REAL_TIME; + } + return TimeBase.CPU_TIME; + } + + @Override + public String getClockSource() { + switch (mClockSource) { + case THREAD_CPU: + return "cpu time"; + case WALL: + return "real time"; + case DUAL: + return "real time, dual clock"; + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java new file mode 100644 index 00000000..6a8721fd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.window.ApplicationWindow; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Properties; + +public class MainWindow extends ApplicationWindow { + + private final static String PING_NAME = "Traceview"; + + private TraceReader mReader; + private String mTraceName; + + // A global cache of string names. + public static HashMap sStringCache = new HashMap(); + + public MainWindow(String traceName, TraceReader reader) { + super(null); + mReader = reader; + mTraceName = traceName; + + addMenuBar(); + } + + public void run() { + setBlockOnOpen(true); + open(); + } + + @Override + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText("Traceview: " + mTraceName); + + InputStream in = getClass().getClassLoader().getResourceAsStream( + "icons/traceview-128.png"); + if (in != null) { + shell.setImage(new Image(shell.getDisplay(), in)); + } + + shell.setBounds(100, 10, 1282, 900); + } + + @Override + protected Control createContents(Composite parent) { + ColorController.assignMethodColors(parent.getDisplay(), mReader.getMethods()); + SelectionController selectionController = new SelectionController(); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + parent.setLayout(gridLayout); + + Display display = parent.getDisplay(); + Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + + // Create a sash form to separate the timeline view (on top) + // and the profile view (on bottom) + SashForm sashForm1 = new SashForm(parent, SWT.VERTICAL); + sashForm1.setBackground(darkGray); + sashForm1.SASH_WIDTH = 3; + GridData data = new GridData(GridData.FILL_BOTH); + sashForm1.setLayoutData(data); + + // Create the timeline view + new TimeLineView(sashForm1, mReader, selectionController); + + // Create the profile view + new ProfileView(sashForm1, mReader, selectionController); + return sashForm1; + } + + @Override + protected MenuManager createMenuManager() { + MenuManager manager = super.createMenuManager(); + + MenuManager viewMenu = new MenuManager("View"); + manager.add(viewMenu); + + Action showPropertiesAction = new Action("Show Properties...") { + @Override + public void run() { + showProperties(); + } + }; + viewMenu.add(showPropertiesAction); + + return manager; + } + + private void showProperties() { + PropertiesDialog dialog = new PropertiesDialog(getShell()); + dialog.setProperties(mReader.getProperties()); + dialog.open(); + } + + /** + * Convert the old two-file format into the current concatenated one. + * + * @param base Base path of the two files, i.e. base.key and base.data + * @return Path to a temporary file that will be deleted on exit. + * @throws IOException + */ + private static String makeTempTraceFile(String base) throws IOException { + // Make a temporary file that will go away on exit and prepare to + // write into it. + File temp = File.createTempFile(base, ".trace"); + temp.deleteOnExit(); + + FileOutputStream dstStream = null; + FileInputStream keyStream = null; + FileInputStream dataStream = null; + + try { + dstStream = new FileOutputStream(temp); + FileChannel dstChannel = dstStream.getChannel(); + + // First copy the contents of the key file into our temp file. + keyStream = new FileInputStream(base + ".key"); + FileChannel srcChannel = keyStream.getChannel(); + long size = dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); + srcChannel.close(); + + // Then concatenate the data file. + dataStream = new FileInputStream(base + ".data"); + srcChannel = dataStream.getChannel(); + dstChannel.transferFrom(srcChannel, size, srcChannel.size()); + } finally { + if (dstStream != null) { + dstStream.close(); // also closes dstChannel + } + if (keyStream != null) { + keyStream.close(); // also closes srcChannel + } + if (dataStream != null) { + dataStream.close(); + } + } + + // Return the path of the temp file. + return temp.getPath(); + } + + /** + * Returns the tools revision number. + */ + private static String getRevision() { + Properties p = new Properties(); + try{ + String toolsdir = System.getProperty("com.android.traceview.toolsdir"); //$NON-NLS-1$ + File sourceProp; + if (toolsdir == null || toolsdir.length() == 0) { + sourceProp = new File("source.properties"); //$NON-NLS-1$ + } else { + sourceProp = new File(toolsdir, "source.properties"); //$NON-NLS-1$ + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(sourceProp); + p.load(fis); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignore) { + } + } + } + + String revision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ + if (revision != null && revision.length() > 0) { + return revision; + } + } catch (FileNotFoundException e) { + // couldn't find the file? don't ping. + } catch (IOException e) { + // couldn't find the file? don't ping. + } + + return null; + } + + + public static void main(String[] args) { + TraceReader reader = null; + boolean regression = false; + + // ping the usage server + + String revision = getRevision(); + if (revision != null) { + new SdkStatsService().ping(PING_NAME, revision); + } + + // Process command line arguments + int argc = 0; + int len = args.length; + while (argc < len) { + String arg = args[argc]; + if (arg.charAt(0) != '-') { + break; + } + if (arg.equals("-r")) { + regression = true; + } else { + break; + } + argc++; + } + if (argc != len - 1) { + System.out.printf("Usage: java %s [-r] trace%n", MainWindow.class.getName()); + System.out.printf(" -r regression only%n"); + return; + } + + String traceName = args[len - 1]; + File file = new File(traceName); + if (file.exists() && file.isDirectory()) { + System.out.printf("Qemu trace files not supported yet.\n"); + System.exit(1); + // reader = new QtraceReader(traceName); + } else { + // If the filename as given doesn't exist... + if (!file.exists()) { + // Try appending .trace. + if (new File(traceName + ".trace").exists()) { + traceName = traceName + ".trace"; + // Next, see if it is the old two-file trace. + } else if (new File(traceName + ".data").exists() + && new File(traceName + ".key").exists()) { + try { + traceName = makeTempTraceFile(traceName); + } catch (IOException e) { + System.err.printf("cannot convert old trace file '%s'\n", traceName); + System.exit(1); + } + // Otherwise, give up. + } else { + System.err.printf("trace file '%s' not found\n", traceName); + System.exit(1); + } + } + + try { + reader = new DmTraceReader(traceName, regression); + } catch (IOException e) { + System.err.printf("Failed to read the trace file"); + e.printStackTrace(); + System.exit(1); + return; + } + } + + reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); + + Display.setAppName("Traceview"); + new MainWindow(traceName, reader).run(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java new file mode 100644 index 00000000..200aa185 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; + +public class MethodData { + + private int mId; + private int mRank = -1; + private String mClassName; + private String mMethodName; + private String mSignature; + private String mName; + private String mProfileName; + private String mPathname; + private int mLineNumber; + private long mElapsedExclusiveCpuTime; + private long mElapsedInclusiveCpuTime; + private long mTopExclusiveCpuTime; + private long mElapsedExclusiveRealTime; + private long mElapsedInclusiveRealTime; + private long mTopExclusiveRealTime; + private int[] mNumCalls = new int[2]; // index 0=normal, 1=recursive + private Color mColor; + private Color mFadedColor; + private Image mImage; + private Image mFadedImage; + private HashMap mParents; + private HashMap mChildren; + + // The parents of this method when this method was in a recursive call + private HashMap mRecursiveParents; + + // The children of this method when this method was in a recursive call + private HashMap mRecursiveChildren; + + private ProfileNode[] mProfileNodes; + private int mX; + private int mY; + private double mWeight; + + public MethodData(int id, String className) { + mId = id; + mClassName = className; + mMethodName = null; + mSignature = null; + mPathname = null; + mLineNumber = -1; + computeName(); + computeProfileName(); + } + + public MethodData(int id, String className, String methodName, + String signature, String pathname, int lineNumber) { + mId = id; + mClassName = className; + mMethodName = methodName; + mSignature = signature; + mPathname = pathname; + mLineNumber = lineNumber; + computeName(); + computeProfileName(); + } + + public double addWeight(int x, int y, double weight) { + if (mX == x && mY == y) + mWeight += weight; + else { + mX = x; + mY = y; + mWeight = weight; + } + return mWeight; + } + + public void clearWeight() { + mWeight = 0; + } + + public int getRank() { + return mRank; + } + + public void setRank(int rank) { + mRank = rank; + computeProfileName(); + } + + public void addElapsedExclusive(long cpuTime, long realTime) { + mElapsedExclusiveCpuTime += cpuTime; + mElapsedExclusiveRealTime += realTime; + } + + public void addElapsedInclusive(long cpuTime, long realTime, + boolean isRecursive, Call parent) { + if (isRecursive == false) { + mElapsedInclusiveCpuTime += cpuTime; + mElapsedInclusiveRealTime += realTime; + mNumCalls[0] += 1; + } else { + mNumCalls[1] += 1; + } + + if (parent == null) + return; + + // Find the child method in the parent + MethodData parentMethod = parent.getMethodData(); + if (parent.isRecursive()) { + parentMethod.mRecursiveChildren = updateInclusive(cpuTime, realTime, + parentMethod, this, false, + parentMethod.mRecursiveChildren); + } else { + parentMethod.mChildren = updateInclusive(cpuTime, realTime, + parentMethod, this, false, parentMethod.mChildren); + } + + // Find the parent method in the child + if (isRecursive) { + mRecursiveParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, + mRecursiveParents); + } else { + mParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, + mParents); + } + } + + private HashMap updateInclusive(long cpuTime, long realTime, + MethodData contextMethod, MethodData elementMethod, + boolean elementIsParent, HashMap map) { + if (map == null) { + map = new HashMap(4); + } else { + ProfileData profileData = map.get(elementMethod.mId); + if (profileData != null) { + profileData.addElapsedInclusive(cpuTime, realTime); + return map; + } + } + + ProfileData elementData = new ProfileData(contextMethod, + elementMethod, elementIsParent); + elementData.setElapsedInclusive(cpuTime, realTime); + elementData.setNumCalls(1); + map.put(elementMethod.mId, elementData); + return map; + } + + public void analyzeData(TimeBase timeBase) { + // Sort the parents and children into decreasing inclusive time + ProfileData[] sortedParents; + ProfileData[] sortedChildren; + ProfileData[] sortedRecursiveParents; + ProfileData[] sortedRecursiveChildren; + + sortedParents = sortProfileData(mParents, timeBase); + sortedChildren = sortProfileData(mChildren, timeBase); + sortedRecursiveParents = sortProfileData(mRecursiveParents, timeBase); + sortedRecursiveChildren = sortProfileData(mRecursiveChildren, timeBase); + + // Add "self" time to the top of the sorted children + sortedChildren = addSelf(sortedChildren); + + // Create the ProfileNode objects that we need + ArrayList nodes = new ArrayList(); + ProfileNode profileNode; + if (mParents != null) { + profileNode = new ProfileNode("Parents", this, sortedParents, + true, false); + nodes.add(profileNode); + } + if (mChildren != null) { + profileNode = new ProfileNode("Children", this, sortedChildren, + false, false); + nodes.add(profileNode); + } + if (mRecursiveParents!= null) { + profileNode = new ProfileNode("Parents while recursive", this, + sortedRecursiveParents, true, true); + nodes.add(profileNode); + } + if (mRecursiveChildren != null) { + profileNode = new ProfileNode("Children while recursive", this, + sortedRecursiveChildren, false, true); + nodes.add(profileNode); + } + mProfileNodes = nodes.toArray(new ProfileNode[nodes.size()]); + } + + // Create and return a ProfileData[] array that is a sorted copy + // of the given HashMap values. + private ProfileData[] sortProfileData(HashMap map, + final TimeBase timeBase) { + if (map == null) + return null; + + // Convert the hash values to an array of ProfileData + Collection values = map.values(); + ProfileData[] sorted = values.toArray(new ProfileData[values.size()]); + + // Sort the array by elapsed inclusive time + Arrays.sort(sorted, new Comparator() { + @Override + public int compare(ProfileData pd1, ProfileData pd2) { + if (timeBase.getElapsedInclusiveTime(pd2) > timeBase.getElapsedInclusiveTime(pd1)) + return 1; + if (timeBase.getElapsedInclusiveTime(pd2) < timeBase.getElapsedInclusiveTime(pd1)) + return -1; + return 0; + } + }); + return sorted; + } + + private ProfileData[] addSelf(ProfileData[] children) { + ProfileData[] pdata; + if (children == null) { + pdata = new ProfileData[1]; + } else { + pdata = new ProfileData[children.length + 1]; + System.arraycopy(children, 0, pdata, 1, children.length); + } + pdata[0] = new ProfileSelf(this); + return pdata; + } + + public void addTopExclusive(long cpuTime, long realTime) { + mTopExclusiveCpuTime += cpuTime; + mTopExclusiveRealTime += realTime; + } + + public long getTopExclusiveCpuTime() { + return mTopExclusiveCpuTime; + } + + public long getTopExclusiveRealTime() { + return mTopExclusiveRealTime; + } + + public int getId() { + return mId; + } + + private void computeName() { + if (mMethodName == null) { + mName = mClassName; + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(mClassName); + sb.append("."); //$NON-NLS-1$ + sb.append(mMethodName); + sb.append(" "); //$NON-NLS-1$ + sb.append(mSignature); + mName = sb.toString(); + } + + public String getName() { + return mName; + } + + public String getClassName() { + return mClassName; + } + + public String getMethodName() { + return mMethodName; + } + + public String getProfileName() { + return mProfileName; + } + + public String getSignature() { + return mSignature; + } + + public void computeProfileName() { + if (mRank == -1) { + mProfileName = mName; + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(mRank); + sb.append(" "); //$NON-NLS-1$ + sb.append(getName()); + mProfileName = sb.toString(); + } + + public String getCalls() { + return String.format("%d+%d", mNumCalls[0], mNumCalls[1]); + } + + public int getTotalCalls() { + return mNumCalls[0] + mNumCalls[1]; + } + + public Color getColor() { + return mColor; + } + + public void setColor(Color color) { + mColor = color; + } + + public void setImage(Image image) { + mImage = image; + } + + public Image getImage() { + return mImage; + } + + @Override + public String toString() { + return getName(); + } + + public long getElapsedExclusiveCpuTime() { + return mElapsedExclusiveCpuTime; + } + + public long getElapsedExclusiveRealTime() { + return mElapsedExclusiveRealTime; + } + + public long getElapsedInclusiveCpuTime() { + return mElapsedInclusiveCpuTime; + } + + public long getElapsedInclusiveRealTime() { + return mElapsedInclusiveRealTime; + } + + public void setFadedColor(Color fadedColor) { + mFadedColor = fadedColor; + } + + public Color getFadedColor() { + return mFadedColor; + } + + public void setFadedImage(Image fadedImage) { + mFadedImage = fadedImage; + } + + public Image getFadedImage() { + return mFadedImage; + } + + public void setPathname(String pathname) { + mPathname = pathname; + } + + public String getPathname() { + return mPathname; + } + + public void setLineNumber(int lineNumber) { + mLineNumber = lineNumber; + } + + public int getLineNumber() { + return mLineNumber; + } + + public ProfileNode[] getProfileNodes() { + return mProfileNodes; + } + + public static class Sorter implements Comparator { + @Override + public int compare(MethodData md1, MethodData md2) { + if (mColumn == Column.BY_NAME) { + int result = md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_INCLUSIVE_CPU_TIME) { + if (md2.getElapsedInclusiveCpuTime() > md1.getElapsedInclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedInclusiveCpuTime() < md1.getElapsedInclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_EXCLUSIVE_CPU_TIME) { + if (md2.getElapsedExclusiveCpuTime() > md1.getElapsedExclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedExclusiveCpuTime() < md1.getElapsedExclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_INCLUSIVE_REAL_TIME) { + if (md2.getElapsedInclusiveRealTime() > md1.getElapsedInclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedInclusiveRealTime() < md1.getElapsedInclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_EXCLUSIVE_REAL_TIME) { + if (md2.getElapsedExclusiveRealTime() > md1.getElapsedExclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedExclusiveRealTime() < md1.getElapsedExclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_CALLS) { + int result = md1.getTotalCalls() - md2.getTotalCalls(); + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_CPU_TIME_PER_CALL) { + double time1 = md1.getElapsedInclusiveCpuTime(); + time1 = time1 / md1.getTotalCalls(); + double time2 = md2.getElapsedInclusiveCpuTime(); + time2 = time2 / md2.getTotalCalls(); + double diff = time1 - time2; + int result = 0; + if (diff < 0) + result = -1; + else if (diff > 0) + result = 1; + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_REAL_TIME_PER_CALL) { + double time1 = md1.getElapsedInclusiveRealTime(); + time1 = time1 / md1.getTotalCalls(); + double time2 = md2.getElapsedInclusiveRealTime(); + time2 = time2 / md2.getTotalCalls(); + double diff = time1 - time2; + int result = 0; + if (diff < 0) + result = -1; + else if (diff > 0) + result = 1; + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + return 0; + } + + public void setColumn(Column column) { + // If the sort column specified is the same as last time, + // then reverse the sort order. + if (mColumn == column) { + // Reverse the sort order + if (mDirection == Direction.INCREASING) + mDirection = Direction.DECREASING; + else + mDirection = Direction.INCREASING; + } else { + // Sort names into increasing order, data into decreasing order. + if (column == Column.BY_NAME) + mDirection = Direction.INCREASING; + else + mDirection = Direction.DECREASING; + } + mColumn = column; + } + + public Column getColumn() { + return mColumn; + } + + public void setDirection(Direction direction) { + mDirection = direction; + } + + public Direction getDirection() { + return mDirection; + } + + public static enum Column { + BY_NAME, BY_EXCLUSIVE_CPU_TIME, BY_EXCLUSIVE_REAL_TIME, + BY_INCLUSIVE_CPU_TIME, BY_INCLUSIVE_REAL_TIME, BY_CALLS, + BY_REAL_TIME_PER_CALL, BY_CPU_TIME_PER_CALL, + }; + + public static enum Direction { + INCREASING, DECREASING + }; + + private Column mColumn; + private Direction mDirection; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java new file mode 100644 index 00000000..dc7e7876 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + + +public class ProfileData { + + protected MethodData mElement; + + /** mContext is either the parent or child of mElement */ + protected MethodData mContext; + protected boolean mElementIsParent; + protected long mElapsedInclusiveCpuTime; + protected long mElapsedInclusiveRealTime; + protected int mNumCalls; + + public ProfileData() { + } + + public ProfileData(MethodData context, MethodData element, + boolean elementIsParent) { + mContext = context; + mElement = element; + mElementIsParent = elementIsParent; + } + + public String getProfileName() { + return mElement.getProfileName(); + } + + public MethodData getMethodData() { + return mElement; + } + + public void addElapsedInclusive(long cpuTime, long realTime) { + mElapsedInclusiveCpuTime += cpuTime; + mElapsedInclusiveRealTime += realTime; + mNumCalls += 1; + } + + public void setElapsedInclusive(long cpuTime, long realTime) { + mElapsedInclusiveCpuTime = cpuTime; + mElapsedInclusiveRealTime = realTime; + } + + public long getElapsedInclusiveCpuTime() { + return mElapsedInclusiveCpuTime; + } + + public long getElapsedInclusiveRealTime() { + return mElapsedInclusiveRealTime; + } + + public void setNumCalls(int numCalls) { + mNumCalls = numCalls; + } + + public String getNumCalls() { + int totalCalls; + if (mElementIsParent) + totalCalls = mContext.getTotalCalls(); + else + totalCalls = mElement.getTotalCalls(); + return String.format("%d/%d", mNumCalls, totalCalls); + } + + public boolean isParent() { + return mElementIsParent; + } + + public MethodData getContext() { + return mContext; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java new file mode 100644 index 00000000..de61702a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +public class ProfileNode { + + private String mLabel; + private MethodData mMethodData; + private ProfileData[] mChildren; + private boolean mIsParent; + private boolean mIsRecursive; + + public ProfileNode(String label, MethodData methodData, + ProfileData[] children, boolean isParent, boolean isRecursive) { + mLabel = label; + mMethodData = methodData; + mChildren = children; + mIsParent = isParent; + mIsRecursive = isRecursive; + } + + public String getLabel() { + return mLabel; + } + + public ProfileData[] getChildren() { + return mChildren; + } + + public boolean isParent() { + return mIsParent; + } + + public boolean isRecursive() { + return mIsRecursive; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java new file mode 100644 index 00000000..8b9ebc8d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.jface.viewers.IColorProvider; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.InputStream; +import java.util.Arrays; + +class ProfileProvider implements ITreeContentProvider { + + private MethodData[] mRoots; + private SelectionAdapter mListener; + private TreeViewer mTreeViewer; + private TraceReader mReader; + private Image mSortUp; + private Image mSortDown; + private String mColumnNames[] = { "Name", + "Incl Cpu Time %", "Incl Cpu Time", "Excl Cpu Time %", "Excl Cpu Time", + "Incl Real Time %", "Incl Real Time", "Excl Real Time %", "Excl Real Time", + "Calls+Recur\nCalls/Total", "Cpu Time/Call", "Real Time/Call" }; + private int mColumnWidths[] = { 370, + 100, 100, 100, 100, + 100, 100, 100, 100, + 100, 100, 100 }; + private int mColumnAlignments[] = { SWT.LEFT, + SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, + SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, + SWT.CENTER, SWT.RIGHT, SWT.RIGHT }; + private static final int COL_NAME = 0; + private static final int COL_INCLUSIVE_CPU_TIME_PER = 1; + private static final int COL_INCLUSIVE_CPU_TIME = 2; + private static final int COL_EXCLUSIVE_CPU_TIME_PER = 3; + private static final int COL_EXCLUSIVE_CPU_TIME = 4; + private static final int COL_INCLUSIVE_REAL_TIME_PER = 5; + private static final int COL_INCLUSIVE_REAL_TIME = 6; + private static final int COL_EXCLUSIVE_REAL_TIME_PER = 7; + private static final int COL_EXCLUSIVE_REAL_TIME = 8; + private static final int COL_CALLS = 9; + private static final int COL_CPU_TIME_PER_CALL = 10; + private static final int COL_REAL_TIME_PER_CALL = 11; + private long mTotalCpuTime; + private long mTotalRealTime; + private int mPrevMatchIndex = -1; + + public ProfileProvider(TraceReader reader) { + mRoots = reader.getMethods(); + mReader = reader; + mTotalCpuTime = reader.getTotalCpuTime(); + mTotalRealTime = reader.getTotalRealTime(); + Display display = Display.getCurrent(); + InputStream in = getClass().getClassLoader().getResourceAsStream( + "icons/sort_up.png"); + mSortUp = new Image(display, in); + in = getClass().getClassLoader().getResourceAsStream( + "icons/sort_down.png"); + mSortDown = new Image(display, in); + } + + private MethodData doMatchName(String name, int startIndex) { + // Check if the given "name" has any uppercase letters + boolean hasUpper = hasUpperCaseCharacter(name); + for (int ii = startIndex; ii < mRoots.length; ++ii) { + MethodData md = mRoots[ii]; + String fullName = md.getName(); + // If there were no upper case letters in the given name, + // then ignore case when matching. + if (!hasUpper) + fullName = fullName.toLowerCase(); + if (fullName.indexOf(name) != -1) { + mPrevMatchIndex = ii; + return md; + } + } + mPrevMatchIndex = -1; + return null; + } + + public MethodData findMatchingName(String name) { + return doMatchName(name, 0); + } + + public MethodData findNextMatchingName(String name) { + return doMatchName(name, mPrevMatchIndex + 1); + } + + public MethodData findMatchingTreeItem(TreeItem item) { + if (item == null) + return null; + String text = item.getText(); + if (Character.isDigit(text.charAt(0)) == false) + return null; + int spaceIndex = text.indexOf(' '); + String numstr = text.substring(0, spaceIndex); + int rank = Integer.valueOf(numstr); + for (MethodData md : mRoots) { + if (md.getRank() == rank) + return md; + } + return null; + } + + public void setTreeViewer(TreeViewer treeViewer) { + mTreeViewer = treeViewer; + } + + public String[] getColumnNames() { + return mColumnNames; + } + + public int[] getColumnWidths() { + int[] widths = Arrays.copyOf(mColumnWidths, mColumnWidths.length); + if (!mReader.haveCpuTime()) { + widths[COL_EXCLUSIVE_CPU_TIME] = 0; + widths[COL_EXCLUSIVE_CPU_TIME_PER] = 0; + widths[COL_INCLUSIVE_CPU_TIME] = 0; + widths[COL_INCLUSIVE_CPU_TIME_PER] = 0; + widths[COL_CPU_TIME_PER_CALL] = 0; + } + if (!mReader.haveRealTime()) { + widths[COL_EXCLUSIVE_REAL_TIME] = 0; + widths[COL_EXCLUSIVE_REAL_TIME_PER] = 0; + widths[COL_INCLUSIVE_REAL_TIME] = 0; + widths[COL_INCLUSIVE_REAL_TIME_PER] = 0; + widths[COL_REAL_TIME_PER_CALL] = 0; + } + return widths; + } + + public int[] getColumnAlignments() { + return mColumnAlignments; + } + + @Override + public Object[] getChildren(Object element) { + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + return md.getProfileNodes(); + } + if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + return pn.getChildren(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof MethodData) + return true; + if (element instanceof ProfileNode) + return true; + return false; + } + + @Override + public Object[] getElements(Object element) { + return mRoots; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + } + + public Object getRoot() { + return "root"; + } + + public SelectionAdapter getColumnListener() { + if (mListener == null) + mListener = new ColumnListener(); + return mListener; + } + + public LabelProvider getLabelProvider() { + return new ProfileLabelProvider(); + } + + class ProfileLabelProvider extends LabelProvider implements + ITableLabelProvider, IColorProvider { + Color colorRed; + Color colorParentsBack; + Color colorChildrenBack; + TraceUnits traceUnits; + + public ProfileLabelProvider() { + Display display = Display.getCurrent(); + colorRed = display.getSystemColor(SWT.COLOR_RED); + colorParentsBack = new Color(display, 230, 230, 255); // blue + colorChildrenBack = new Color(display, 255, 255, 210); // yellow + traceUnits = mReader.getTraceUnits(); + } + + @Override + public String getColumnText(Object element, int col) { + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + if (col == COL_NAME) + return md.getProfileName(); + if (col == COL_EXCLUSIVE_CPU_TIME) { + double val = md.getElapsedExclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_EXCLUSIVE_CPU_TIME_PER) { + double val = md.getElapsedExclusiveCpuTime(); + double per = val * 100.0 / mTotalCpuTime; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = md.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double val = md.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / mTotalCpuTime; + return String.format("%.1f%%", per); + } + if (col == COL_EXCLUSIVE_REAL_TIME) { + double val = md.getElapsedExclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_EXCLUSIVE_REAL_TIME_PER) { + double val = md.getElapsedExclusiveRealTime(); + double per = val * 100.0 / mTotalRealTime; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = md.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double val = md.getElapsedInclusiveRealTime(); + double per = val * 100.0 / mTotalRealTime; + return String.format("%.1f%%", per); + } + if (col == COL_CALLS) + return md.getCalls(); + if (col == COL_CPU_TIME_PER_CALL) { + int numCalls = md.getTotalCalls(); + double val = md.getElapsedInclusiveCpuTime(); + val = val / numCalls; + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_REAL_TIME_PER_CALL) { + int numCalls = md.getTotalCalls(); + double val = md.getElapsedInclusiveRealTime(); + val = val / numCalls; + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + } else if (element instanceof ProfileSelf) { + ProfileSelf ps = (ProfileSelf) element; + if (col == COL_NAME) + return ps.getProfileName(); + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = ps.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double total; + double val = ps.getElapsedInclusiveCpuTime(); + MethodData context = ps.getContext(); + total = context.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = ps.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double total; + double val = ps.getElapsedInclusiveRealTime(); + MethodData context = ps.getContext(); + total = context.getElapsedInclusiveRealTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + return ""; + } else if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + if (col == COL_NAME) + return pd.getProfileName(); + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = pd.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double total; + double val = pd.getElapsedInclusiveCpuTime(); + MethodData context = pd.getContext(); + total = context.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = pd.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double total; + double val = pd.getElapsedInclusiveRealTime(); + MethodData context = pd.getContext(); + total = context.getElapsedInclusiveRealTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_CALLS) + return pd.getNumCalls(); + return ""; + } else if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + if (col == COL_NAME) + return pn.getLabel(); + return ""; + } + return "col" + col; + } + + @Override + public Image getColumnImage(Object element, int col) { + if (col != COL_NAME) + return null; + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + return md.getImage(); + } + if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + MethodData md = pd.getMethodData(); + return md.getImage(); + } + return null; + } + + @Override + public Color getForeground(Object element) { + return null; + } + + @Override + public Color getBackground(Object element) { + if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + if (pd.isParent()) + return colorParentsBack; + return colorChildrenBack; + } + if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + if (pn.isParent()) + return colorParentsBack; + return colorChildrenBack; + } + return null; + } + } + + class ColumnListener extends SelectionAdapter { + MethodData.Sorter sorter = new MethodData.Sorter(); + + @Override + public void widgetSelected(SelectionEvent event) { + TreeColumn column = (TreeColumn) event.widget; + String name = column.getText(); + Tree tree = column.getParent(); + tree.setRedraw(false); + TreeColumn[] columns = tree.getColumns(); + for (TreeColumn col : columns) { + col.setImage(null); + } + if (name == mColumnNames[COL_NAME]) { + // Sort names alphabetically + sorter.setColumn(MethodData.Sorter.Column.BY_NAME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_CALLS]) { + sorter.setColumn(MethodData.Sorter.Column.BY_CALLS); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_CPU_TIME_PER_CALL]) { + sorter.setColumn(MethodData.Sorter.Column.BY_CPU_TIME_PER_CALL); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_REAL_TIME_PER_CALL]) { + sorter.setColumn(MethodData.Sorter.Column.BY_REAL_TIME_PER_CALL); + Arrays.sort(mRoots, sorter); + } + MethodData.Sorter.Direction direction = sorter.getDirection(); + if (direction == MethodData.Sorter.Direction.INCREASING) + column.setImage(mSortDown); + else + column.setImage(mSortUp); + tree.setRedraw(true); + mTreeViewer.refresh(); + } + } + + public static boolean hasUpperCaseCharacter(String s) + { + for (int i = 0; i < s.length(); i++) { + if (Character.isUpperCase(s.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java new file mode 100644 index 00000000..f98b6b2f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +public class ProfileSelf extends ProfileData { + public ProfileSelf(MethodData methodData) { + mElement = methodData; + mContext = methodData; + } + + @Override + public String getProfileName() { + return "self"; + } + + @Override + public long getElapsedInclusiveCpuTime() { + return mElement.getTopExclusiveCpuTime(); + } + + @Override + public long getElapsedInclusiveRealTime() { + return mElement.getTopExclusiveRealTime(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java new file mode 100644 index 00000000..60eb5cbd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeViewerListener; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeExpansionEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.util.ArrayList; +import java.util.Observable; +import java.util.Observer; + +public class ProfileView extends Composite implements Observer { + + private TreeViewer mTreeViewer; + private Text mSearchBox; + private SelectionController mSelectionController; + private ProfileProvider mProfileProvider; + private Color mColorNoMatch; + private Color mColorMatch; + private MethodData mCurrentHighlightedMethod; + private MethodHandler mMethodHandler; + + public interface MethodHandler { + void handleMethod(MethodData method); + } + + public ProfileView(Composite parent, TraceReader reader, + SelectionController selectController) { + super(parent, SWT.NONE); + setLayout(new GridLayout(1, false)); + this.mSelectionController = selectController; + mSelectionController.addObserver(this); + + // Add a tree viewer at the top + mTreeViewer = new TreeViewer(this, SWT.MULTI | SWT.NONE); + mTreeViewer.setUseHashlookup(true); + mProfileProvider = reader.getProfileProvider(); + mProfileProvider.setTreeViewer(mTreeViewer); + SelectionAdapter listener = mProfileProvider.getColumnListener(); + final Tree tree = mTreeViewer.getTree(); + tree.setHeaderVisible(true); + tree.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // Get the column names from the ProfileProvider + String[] columnNames = mProfileProvider.getColumnNames(); + int[] columnWidths = mProfileProvider.getColumnWidths(); + int[] columnAlignments = mProfileProvider.getColumnAlignments(); + for (int ii = 0; ii < columnWidths.length; ++ii) { + TreeColumn column = new TreeColumn(tree, SWT.LEFT); + column.setText(columnNames[ii]); + column.setWidth(columnWidths[ii]); + column.setMoveable(true); + column.addSelectionListener(listener); + column.setAlignment(columnAlignments[ii]); + } + + // Add a listener to the tree so that we can make the row + // height smaller. + tree.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + int fontHeight = event.gc.getFontMetrics().getHeight(); + event.height = fontHeight; + } + }); + + mTreeViewer.setContentProvider(mProfileProvider); + mTreeViewer.setLabelProvider(mProfileProvider.getLabelProvider()); + mTreeViewer.setInput(mProfileProvider.getRoot()); + + // Create another composite to hold the label and text box + Composite composite = new Composite(this, SWT.NONE); + composite.setLayout(new GridLayout(2, false)); + composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // Add a label for the search box + Label label = new Label(composite, SWT.NONE); + label.setText("Find:"); + + // Add a text box for searching for method names + mSearchBox = new Text(composite, SWT.BORDER); + mSearchBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Display display = getDisplay(); + mColorNoMatch = new Color(display, 255, 200, 200); + mColorMatch = mSearchBox.getBackground(); + + mSearchBox.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent ev) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findName(query); + } + }); + + // Add a key listener to the text box so that we can clear + // the text box if the user presses . + mSearchBox.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.keyCode == SWT.ESC) { + mSearchBox.setText(""); + } else if (event.keyCode == SWT.CR) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findNextName(query); + } + } + }); + + // Also add a key listener to the tree viewer so that the + // user can just start typing anywhere in the tree view. + tree.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.keyCode == SWT.ESC) { + mSearchBox.setText(""); + } else if (event.keyCode == SWT.BS) { + // Erase the last character from the search box + String text = mSearchBox.getText(); + int len = text.length(); + String chopped; + if (len <= 1) + chopped = ""; + else + chopped = text.substring(0, len - 1); + mSearchBox.setText(chopped); + } else if (event.keyCode == SWT.CR) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findNextName(query); + } else { + // Append the given character to the search box + String str = String.valueOf(event.character); + mSearchBox.append(str); + } + event.doit = false; + } + }); + + // Add a selection listener to the tree so that the user can click + // on a method that is a child or parent and jump to that method. + mTreeViewer + .addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent ev) { + ISelection sel = ev.getSelection(); + if (sel.isEmpty()) + return; + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + highlightMethod(md, true); + } + if (element instanceof ProfileData) { + MethodData md = ((ProfileData) element) + .getMethodData(); + highlightMethod(md, true); + } + } + } + }); + + // Add a tree listener so that we can expand the parents and children + // of a method when a method is expanded. + mTreeViewer.addTreeListener(new ITreeViewerListener() { + @Override + public void treeExpanded(TreeExpansionEvent event) { + Object element = event.getElement(); + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + expandNode(md); + } + } + @Override + public void treeCollapsed(TreeExpansionEvent event) { + } + }); + + tree.addListener(SWT.MouseDown, new Listener() { + @Override + public void handleEvent(Event event) { + Point point = new Point(event.x, event.y); + TreeItem treeItem = tree.getItem(point); + MethodData md = mProfileProvider.findMatchingTreeItem(treeItem); + if (md == null) + return; + ArrayList selections = new ArrayList(); + selections.add(Selection.highlight("MethodData", md)); + mSelectionController.change(selections, "ProfileView"); + + if (mMethodHandler != null && (event.stateMask & SWT.MOD1) != 0) { + mMethodHandler.handleMethod(md); + } + } + }); + } + + public void setMethodHandler(MethodHandler handler) { + mMethodHandler = handler; + } + + private void findName(String query) { + MethodData md = mProfileProvider.findMatchingName(query); + selectMethod(md); + } + + private void findNextName(String query) { + MethodData md = mProfileProvider.findNextMatchingName(query); + selectMethod(md); + } + + private void selectMethod(MethodData md) { + if (md == null) { + mSearchBox.setBackground(mColorNoMatch); + return; + } + mSearchBox.setBackground(mColorMatch); + highlightMethod(md, false); + } + + @Override + public void update(Observable objservable, Object arg) { + // Ignore updates from myself + if (arg == "ProfileView") + return; + // System.out.printf("profileview update from %s\n", arg); + ArrayList selections; + selections = mSelectionController.getSelections(); + for (Selection selection : selections) { + Selection.Action action = selection.getAction(); + if (action != Selection.Action.Highlight) + continue; + String name = selection.getName(); + if (name == "MethodData") { + MethodData md = (MethodData) selection.getValue(); + highlightMethod(md, true); + return; + } + if (name == "Call") { + Call call = (Call) selection.getValue(); + MethodData md = call.getMethodData(); + highlightMethod(md, true); + return; + } + } + } + + private void highlightMethod(MethodData md, boolean clearSearch) { + if (md == null) + return; + // Avoid an infinite recursion + if (md == mCurrentHighlightedMethod) + return; + if (clearSearch) { + mSearchBox.setText(""); + mSearchBox.setBackground(mColorMatch); + } + mCurrentHighlightedMethod = md; + mTreeViewer.collapseAll(); + // Expand this node and its children + expandNode(md); + StructuredSelection sel = new StructuredSelection(md); + mTreeViewer.setSelection(sel, true); + Tree tree = mTreeViewer.getTree(); + TreeItem[] items = tree.getSelection(); + if (items.length != 0) { + tree.setTopItem(items[0]); + // workaround a Mac bug by adding showItem(). + tree.showItem(items[0]); + } + } + + private void expandNode(MethodData md) { + ProfileNode[] nodes = md.getProfileNodes(); + mTreeViewer.setExpandedState(md, true); + // Also expand the "Parents" and "Children" nodes. + if (nodes != null) { + for (ProfileNode node : nodes) { + if (node.isRecursive() == false) + mTreeViewer.setExpandedState(node, true); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java new file mode 100644 index 00000000..cc0cce06 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; + +import java.util.HashMap; +import java.util.Map.Entry; + +public class PropertiesDialog extends Dialog { + private HashMap mProperties; + + public PropertiesDialog(Shell parent) { + super(parent); + + setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE); + } + + public void setProperties(HashMap properties) { + mProperties = properties; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + container.setLayout(gridLayout); + + TableViewer tableViewer = new TableViewer(container, SWT.HIDE_SELECTION + | SWT.V_SCROLL | SWT.BORDER); + tableViewer.getTable().setLinesVisible(true); + tableViewer.getTable().setHeaderVisible(true); + + TableViewerColumn propertyColumn = new TableViewerColumn(tableViewer, SWT.NONE); + propertyColumn.getColumn().setText("Property"); + propertyColumn.setLabelProvider(new ColumnLabelProvider() { + @Override + @SuppressWarnings("unchecked") + public String getText(Object element) { + Entry entry = (Entry) element; + return entry.getKey(); + } + }); + propertyColumn.getColumn().setWidth(400); + + TableViewerColumn valueColumn = new TableViewerColumn(tableViewer, SWT.NONE); + valueColumn.getColumn().setText("Value"); + valueColumn.setLabelProvider(new ColumnLabelProvider() { + @Override + @SuppressWarnings("unchecked") + public String getText(Object element) { + Entry entry = (Entry) element; + return entry.getValue(); + } + }); + valueColumn.getColumn().setWidth(200); + + tableViewer.setContentProvider(new ArrayContentProvider()); + tableViewer.setInput(mProperties.entrySet().toArray()); + + GridData gridData = new GridData(); + gridData.verticalAlignment = GridData.FILL; + gridData.horizontalAlignment = GridData.FILL; + gridData.grabExcessHorizontalSpace = true; + gridData.grabExcessVerticalSpace = true; + tableViewer.getControl().setLayoutData(gridData); + + return container; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java new file mode 100644 index 00000000..7d2e47f3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +public class Selection { + + private Action mAction; + private String mName; + private Object mValue; + + public Selection(Action action, String name, Object value) { + mAction = action; + mName = name; + mValue = value; + } + + public static Selection highlight(String name, Object value) { + return new Selection(Action.Highlight, name, value); + } + + public static Selection include(String name, Object value) { + return new Selection(Action.Include, name, value); + } + + public static Selection exclude(String name, Object value) { + return new Selection(Action.Exclude, name, value); + } + + public void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + public void setValue(Object value) { + mValue = value; + } + + public Object getValue() { + return mValue; + } + + public void setAction(Action action) { + mAction = action; + } + + public Action getAction() { + return mAction; + } + + public static enum Action { + Highlight, Include, Exclude, Aggregate + }; +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java new file mode 100644 index 00000000..daf22149 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.util.ArrayList; +import java.util.Observable; + +public class SelectionController extends Observable { + + private ArrayList mSelections; + + public void change(ArrayList selections, Object arg) { + this.mSelections = selections; + setChanged(); + notifyObservers(arg); + } + + public ArrayList getSelections() { + return mSelections; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java new file mode 100644 index 00000000..023ae2bf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.util.ArrayList; +import java.util.HashMap; + +class ThreadData implements TimeLineView.Row { + + private int mId; + private String mName; + private boolean mIsEmpty; + + private Call mRootCall; + private ArrayList mStack = new ArrayList(); + + // This is a hash of all the methods that are currently on the stack. + private HashMap mStackMethods = new HashMap(); + + boolean mHaveGlobalTime; + long mGlobalStartTime; + long mGlobalEndTime; + + boolean mHaveThreadTime; + long mThreadStartTime; + long mThreadEndTime; + + long mThreadCurrentTime; // only used while parsing thread-cpu clock + + ThreadData(int id, String name, MethodData topLevel) { + mId = id; + mName = String.format("[%d] %s", id, name); + mIsEmpty = true; + mRootCall = new Call(this, topLevel, null); + mRootCall.setName(mName); + mStack.add(mRootCall); + } + + @Override + public String getName() { + return mName; + } + + public Call getRootCall() { + return mRootCall; + } + + /** + * Returns true if no calls have ever been recorded for this thread. + */ + public boolean isEmpty() { + return mIsEmpty; + } + + Call enter(MethodData method, ArrayList trace) { + if (mIsEmpty) { + mIsEmpty = false; + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_ENTER, mRootCall)); + } + } + + Call caller = top(); + Call call = new Call(this, method, caller); + mStack.add(call); + + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_ENTER, call)); + } + + Integer num = mStackMethods.get(method); + if (num == null) { + num = 0; + } else if (num > 0) { + call.setRecursive(true); + } + mStackMethods.put(method, num + 1); + + return call; + } + + Call exit(MethodData method, ArrayList trace) { + Call call = top(); + if (call.mCaller == null) { + return null; + } + + if (call.getMethodData() != method) { + String error = "Method exit (" + method.getName() + + ") does not match current method (" + call.getMethodData().getName() + + ")"; + throw new RuntimeException(error); + } + + mStack.remove(mStack.size() - 1); + + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_EXIT, call)); + } + + Integer num = mStackMethods.get(method); + if (num != null) { + if (num == 1) { + mStackMethods.remove(method); + } else { + mStackMethods.put(method, num - 1); + } + } + + return call; + } + + Call top() { + return mStack.get(mStack.size() - 1); + } + + void endTrace(ArrayList trace) { + for (int i = mStack.size() - 1; i >= 1; i--) { + Call call = mStack.get(i); + call.mGlobalEndTime = mGlobalEndTime; + call.mThreadEndTime = mThreadEndTime; + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_INCOMPLETE, call)); + } + } + mStack.clear(); + mStackMethods.clear(); + } + + void updateRootCallTimeBounds() { + if (!mIsEmpty) { + mRootCall.mGlobalStartTime = mGlobalStartTime; + mRootCall.mGlobalEndTime = mGlobalEndTime; + mRootCall.mThreadStartTime = mThreadStartTime; + mRootCall.mThreadEndTime = mThreadEndTime; + } + } + + @Override + public String toString() { + return mName; + } + + @Override + public int getId() { + return mId; + } + + public long getCpuTime() { + return mRootCall.mInclusiveCpuTime; + } + + public long getRealTime() { + return mRootCall.mInclusiveRealTime; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java new file mode 100644 index 00000000..1f3fa0f7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +class TickScaler { + + private double mMinVal; // required input + private double mMaxVal; // required input + private double mRangeVal; + private int mNumPixels; // required input + private int mPixelsPerTick; // required input + private double mPixelsPerRange; + private double mTickIncrement; + private double mMinMajorTick; + + TickScaler(double minVal, double maxVal, int numPixels, int pixelsPerTick) { + mMinVal = minVal; + mMaxVal = maxVal; + mNumPixels = numPixels; + mPixelsPerTick = pixelsPerTick; + } + + public void setMinVal(double minVal) { + mMinVal = minVal; + } + + public double getMinVal() { + return mMinVal; + } + + public void setMaxVal(double maxVal) { + mMaxVal = maxVal; + } + + public double getMaxVal() { + return mMaxVal; + } + + public void setNumPixels(int numPixels) { + mNumPixels = numPixels; + } + + public int getNumPixels() { + return mNumPixels; + } + + public void setPixelsPerTick(int pixelsPerTick) { + mPixelsPerTick = pixelsPerTick; + } + + public int getPixelsPerTick() { + return mPixelsPerTick; + } + + public void setPixelsPerRange(double pixelsPerRange) { + mPixelsPerRange = pixelsPerRange; + } + + public double getPixelsPerRange() { + return mPixelsPerRange; + } + + public void setTickIncrement(double tickIncrement) { + mTickIncrement = tickIncrement; + } + + public double getTickIncrement() { + return mTickIncrement; + } + + public void setMinMajorTick(double minMajorTick) { + mMinMajorTick = minMajorTick; + } + + public double getMinMajorTick() { + return mMinMajorTick; + } + + // Convert a time value to a 0-based pixel value + public int valueToPixel(double value) { + return (int) Math.ceil(mPixelsPerRange * (value - mMinVal) - 0.5); + } + + // Convert a time value to a 0-based fractional pixel + public double valueToPixelFraction(double value) { + return mPixelsPerRange * (value - mMinVal); + } + + // Convert a 0-based pixel value to a time value + public double pixelToValue(int pixel) { + return mMinVal + (pixel / mPixelsPerRange); + } + + public void computeTicks(boolean useGivenEndPoints) { + int numTicks = mNumPixels / mPixelsPerTick; + mRangeVal = mMaxVal - mMinVal; + mTickIncrement = mRangeVal / numTicks; + double dlogTickIncrement = Math.log10(mTickIncrement); + int logTickIncrement = (int) Math.floor(dlogTickIncrement); + double scale = Math.pow(10, logTickIncrement); + double scaledTickIncr = mTickIncrement / scale; + if (scaledTickIncr > 5.0) + scaledTickIncr = 10; + else if (scaledTickIncr > 2) + scaledTickIncr = 5; + else if (scaledTickIncr > 1) + scaledTickIncr = 2; + else + scaledTickIncr = 1; + mTickIncrement = scaledTickIncr * scale; + + if (!useGivenEndPoints) { + // Round up the max val to the next minor tick + double minorTickIncrement = mTickIncrement / 5; + double dval = mMaxVal / minorTickIncrement; + int ival = (int) dval; + if (ival != dval) + mMaxVal = (ival + 1) * minorTickIncrement; + + // Round down the min val to a multiple of tickIncrement + ival = (int) (mMinVal / mTickIncrement); + mMinVal = ival * mTickIncrement; + mMinMajorTick = mMinVal; + } else { + int ival = (int) (mMinVal / mTickIncrement); + mMinMajorTick = ival * mTickIncrement; + if (mMinMajorTick < mMinVal) + mMinMajorTick = mMinMajorTick + mTickIncrement; + } + + mRangeVal = mMaxVal - mMinVal; + mPixelsPerRange = (double) mNumPixels / mRangeVal; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java new file mode 100644 index 00000000..b9a53753 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +interface TimeBase { + public static final TimeBase CPU_TIME = new CpuTimeBase(); + public static final TimeBase REAL_TIME = new RealTimeBase(); + + public long getTime(ThreadData threadData); + public long getElapsedInclusiveTime(MethodData methodData); + public long getElapsedExclusiveTime(MethodData methodData); + public long getElapsedInclusiveTime(ProfileData profileData); + + public static final class CpuTimeBase implements TimeBase { + @Override + public long getTime(ThreadData threadData) { + return threadData.getCpuTime(); + } + + @Override + public long getElapsedInclusiveTime(MethodData methodData) { + return methodData.getElapsedInclusiveCpuTime(); + } + + @Override + public long getElapsedExclusiveTime(MethodData methodData) { + return methodData.getElapsedExclusiveCpuTime(); + } + + @Override + public long getElapsedInclusiveTime(ProfileData profileData) { + return profileData.getElapsedInclusiveCpuTime(); + } + } + + public static final class RealTimeBase implements TimeBase { + @Override + public long getTime(ThreadData threadData) { + return threadData.getRealTime(); + } + + @Override + public long getElapsedInclusiveTime(MethodData methodData) { + return methodData.getElapsedInclusiveRealTime(); + } + + @Override + public long getElapsedExclusiveTime(MethodData methodData) { + return methodData.getElapsedExclusiveRealTime(); + } + + @Override + public long getElapsedInclusiveTime(ProfileData profileData) { + return profileData.getElapsedInclusiveRealTime(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java new file mode 100644 index 00000000..518f676d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java @@ -0,0 +1,2154 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import org.eclipse.jface.resource.FontRegistry; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.ScrollBar; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; + +public class TimeLineView extends Composite implements Observer { + + private HashMap mRowByName; + private RowData[] mRows; + private Segment[] mSegments; + private HashMap mThreadLabels; + private Timescale mTimescale; + private Surface mSurface; + private RowLabels mLabels; + private SashForm mSashForm; + private int mScrollOffsetY; + + public static final int PixelsPerTick = 50; + private TickScaler mScaleInfo = new TickScaler(0, 0, 0, PixelsPerTick); + private static final int LeftMargin = 10; // blank space on left + private static final int RightMargin = 60; // blank space on right + + private Color mColorBlack; + private Color mColorGray; + private Color mColorDarkGray; + private Color mColorForeground; + private Color mColorRowBack; + private Color mColorZoomSelection; + private FontRegistry mFontRegistry; + + /** vertical height of drawn blocks in each row */ + private static final int rowHeight = 20; + + /** the blank space between rows */ + private static final int rowYMargin = 12; + private static final int rowYMarginHalf = rowYMargin / 2; + + /** total vertical space for row */ + private static final int rowYSpace = rowHeight + rowYMargin; + private static final int majorTickLength = 8; + private static final int minorTickLength = 4; + private static final int timeLineOffsetY = 58; + private static final int tickToFontSpacing = 2; + + /** start of first row */ + private static final int topMargin = 90; + private int mMouseRow = -1; + private int mNumRows; + private int mStartRow; + private int mEndRow; + private TraceUnits mUnits; + private String mClockSource; + private boolean mHaveCpuTime; + private boolean mHaveRealTime; + private int mSmallFontWidth; + private int mSmallFontHeight; + private SelectionController mSelectionController; + private MethodData mHighlightMethodData; + private Call mHighlightCall; + private static final int MinInclusiveRange = 3; + + /** Setting the fonts looks good on Linux but bad on Macs */ + private boolean mSetFonts = false; + + public static interface Block { + public String getName(); + public MethodData getMethodData(); + public long getStartTime(); + public long getEndTime(); + public Color getColor(); + public double addWeight(int x, int y, double weight); + public void clearWeight(); + public long getExclusiveCpuTime(); + public long getInclusiveCpuTime(); + public long getExclusiveRealTime(); + public long getInclusiveRealTime(); + public boolean isContextSwitch(); + public boolean isIgnoredBlock(); + public Block getParentBlock(); + } + + public static interface Row { + public int getId(); + public String getName(); + } + + public static class Record { + Row row; + Block block; + + public Record(Row row, Block block) { + this.row = row; + this.block = block; + } + } + + public TimeLineView(Composite parent, TraceReader reader, + SelectionController selectionController) { + super(parent, SWT.NONE); + mRowByName = new HashMap(); + this.mSelectionController = selectionController; + selectionController.addObserver(this); + mUnits = reader.getTraceUnits(); + mClockSource = reader.getClockSource(); + mHaveCpuTime = reader.haveCpuTime(); + mHaveRealTime = reader.haveRealTime(); + mThreadLabels = reader.getThreadLabels(); + + Display display = getDisplay(); + mColorGray = display.getSystemColor(SWT.COLOR_GRAY); + mColorDarkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + mColorBlack = display.getSystemColor(SWT.COLOR_BLACK); + // mColorBackground = display.getSystemColor(SWT.COLOR_WHITE); + mColorForeground = display.getSystemColor(SWT.COLOR_BLACK); + mColorRowBack = new Color(display, 240, 240, 255); + mColorZoomSelection = new Color(display, 230, 230, 230); + + mFontRegistry = new FontRegistry(display); + mFontRegistry.put("small", //$NON-NLS-1$ + new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ + mFontRegistry.put("courier8", //$NON-NLS-1$ + new FontData[] { new FontData("Courier New", 8, SWT.BOLD) }); //$NON-NLS-1$ + mFontRegistry.put("medium", //$NON-NLS-1$ + new FontData[] { new FontData("Courier New", 10, SWT.NORMAL) }); //$NON-NLS-1$ + + Image image = new Image(display, new Rectangle(100, 100, 100, 100)); + GC gc = new GC(image); + if (mSetFonts) { + gc.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ + } + mSmallFontWidth = gc.getFontMetrics().getAverageCharWidth(); + mSmallFontHeight = gc.getFontMetrics().getHeight(); + + image.dispose(); + gc.dispose(); + + setLayout(new FillLayout()); + + // Create a sash form for holding two canvas views, one for the + // thread labels and one for the thread timeline. + mSashForm = new SashForm(this, SWT.HORIZONTAL); + mSashForm.setBackground(mColorGray); + mSashForm.SASH_WIDTH = 3; + + // Create a composite for the left side of the sash + Composite composite = new Composite(mSashForm, SWT.NONE); + GridLayout layout = new GridLayout(1, true /* make columns equal width */); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 1; + composite.setLayout(layout); + + // Create a blank corner space in the upper left corner + BlankCorner corner = new BlankCorner(composite); + GridData gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.heightHint = topMargin; + corner.setLayoutData(gridData); + + // Add the thread labels below the blank corner. + mLabels = new RowLabels(composite); + gridData = new GridData(GridData.FILL_BOTH); + mLabels.setLayoutData(gridData); + + // Create another composite for the right side of the sash + composite = new Composite(mSashForm, SWT.NONE); + layout = new GridLayout(1, true /* make columns equal width */); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 1; + composite.setLayout(layout); + + mTimescale = new Timescale(composite); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.heightHint = topMargin; + mTimescale.setLayoutData(gridData); + + mSurface = new Surface(composite); + gridData = new GridData(GridData.FILL_BOTH); + mSurface.setLayoutData(gridData); + mSashForm.setWeights(new int[] { 1, 5 }); + + final ScrollBar vBar = mSurface.getVerticalBar(); + vBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + mScrollOffsetY = vBar.getSelection(); + Point dim = mSurface.getSize(); + int newScrollOffsetY = computeVisibleRows(dim.y); + if (newScrollOffsetY != mScrollOffsetY) { + mScrollOffsetY = newScrollOffsetY; + vBar.setSelection(newScrollOffsetY); + } + mLabels.redraw(); + mSurface.redraw(); + } + }); + + final ScrollBar hBar = mSurface.getHorizontalBar(); + hBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + mSurface.setScaleFromHorizontalScrollBar(hBar.getSelection()); + mSurface.redraw(); + } + }); + + mSurface.addListener(SWT.Resize, new Listener() { + @Override + public void handleEvent(Event e) { + Point dim = mSurface.getSize(); + + // If we don't need the scroll bar then don't display it. + if (dim.y >= mNumRows * rowYSpace) { + vBar.setVisible(false); + } else { + vBar.setVisible(true); + } + int newScrollOffsetY = computeVisibleRows(dim.y); + if (newScrollOffsetY != mScrollOffsetY) { + mScrollOffsetY = newScrollOffsetY; + vBar.setSelection(newScrollOffsetY); + } + + int spaceNeeded = mNumRows * rowYSpace; + vBar.setMaximum(spaceNeeded); + vBar.setThumb(dim.y); + + mLabels.redraw(); + mSurface.redraw(); + } + }); + + mSurface.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent me) { + mSurface.mouseUp(me); + } + + @Override + public void mouseDown(MouseEvent me) { + mSurface.mouseDown(me); + } + + @Override + public void mouseDoubleClick(MouseEvent me) { + mSurface.mouseDoubleClick(me); + } + }); + + mSurface.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mSurface.mouseMove(me); + } + }); + + mSurface.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent me) { + mSurface.mouseScrolled(me); + } + }); + + mTimescale.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent me) { + mTimescale.mouseUp(me); + } + + @Override + public void mouseDown(MouseEvent me) { + mTimescale.mouseDown(me); + } + + @Override + public void mouseDoubleClick(MouseEvent me) { + mTimescale.mouseDoubleClick(me); + } + }); + + mTimescale.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mTimescale.mouseMove(me); + } + }); + + mLabels.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mLabels.mouseMove(me); + } + }); + + setData(reader.getThreadTimeRecords()); + } + + @Override + public void update(Observable objservable, Object arg) { + // Ignore updates from myself + if (arg == "TimeLineView") //$NON-NLS-1$ + return; + // System.out.printf("timeline update from %s\n", arg); + boolean foundHighlight = false; + ArrayList selections; + selections = mSelectionController.getSelections(); + for (Selection selection : selections) { + Selection.Action action = selection.getAction(); + if (action != Selection.Action.Highlight) + continue; + String name = selection.getName(); + // System.out.printf(" timeline highlight %s from %s\n", name, arg); + if (name == "MethodData") { //$NON-NLS-1$ + foundHighlight = true; + mHighlightMethodData = (MethodData) selection.getValue(); + // System.out.printf(" method %s\n", + // highlightMethodData.getName()); + mHighlightCall = null; + startHighlighting(); + } else if (name == "Call") { //$NON-NLS-1$ + foundHighlight = true; + mHighlightCall = (Call) selection.getValue(); + // System.out.printf(" call %s\n", highlightCall.getName()); + mHighlightMethodData = null; + startHighlighting(); + } + } + if (foundHighlight == false) + mSurface.clearHighlights(); + } + + public void setData(ArrayList records) { + if (records == null) + records = new ArrayList(); + + if (false) { + System.out.println("TimelineView() list of records:"); //$NON-NLS-1$ + for (Record r : records) { + System.out.printf("row '%s' block '%s' [%d, %d]\n", r.row //$NON-NLS-1$ + .getName(), r.block.getName(), r.block.getStartTime(), + r.block.getEndTime()); + if (r.block.getStartTime() > r.block.getEndTime()) { + System.err.printf("Error: block startTime > endTime\n"); //$NON-NLS-1$ + System.exit(1); + } + } + } + + // Sort the records into increasing start time, and decreasing end time + Collections.sort(records, new Comparator() { + @Override + public int compare(Record r1, Record r2) { + long start1 = r1.block.getStartTime(); + long start2 = r2.block.getStartTime(); + if (start1 > start2) + return 1; + if (start1 < start2) + return -1; + + // The start times are the same, so compare the end times + long end1 = r1.block.getEndTime(); + long end2 = r2.block.getEndTime(); + if (end1 > end2) + return -1; + if (end1 < end2) + return 1; + + return 0; + } + }); + + ArrayList segmentList = new ArrayList(); + + // The records are sorted into increasing start time, + // so the minimum start time is the start time of the first record. + double minVal = 0; + if (records.size() > 0) + minVal = records.get(0).block.getStartTime(); + + // Sum the time spent in each row and block, and + // keep track of the maximum end time. + double maxVal = 0; + for (Record rec : records) { + Row row = rec.row; + Block block = rec.block; + if (block.isIgnoredBlock()) { + continue; + } + + String rowName = row.getName(); + RowData rd = mRowByName.get(rowName); + if (rd == null) { + rd = new RowData(row); + mRowByName.put(rowName, rd); + } + long blockStartTime = block.getStartTime(); + long blockEndTime = block.getEndTime(); + if (blockEndTime > rd.mEndTime) { + long start = Math.max(blockStartTime, rd.mEndTime); + rd.mElapsed += blockEndTime - start; + rd.mEndTime = blockEndTime; + } + if (blockEndTime > maxVal) + maxVal = blockEndTime; + + // Keep track of nested blocks by using a stack (for each row). + // Create a Segment object for each visible part of a block. + Block top = rd.top(); + if (top == null) { + rd.push(block); + continue; + } + + long topStartTime = top.getStartTime(); + long topEndTime = top.getEndTime(); + if (topEndTime >= blockStartTime) { + // Add this segment if it has a non-zero elapsed time. + if (topStartTime < blockStartTime) { + Segment segment = new Segment(rd, top, topStartTime, + blockStartTime); + segmentList.add(segment); + } + + // If this block starts where the previous (top) block ends, + // then pop off the top block. + if (topEndTime == blockStartTime) + rd.pop(); + rd.push(block); + } else { + // We may have to pop several frames here. + popFrames(rd, top, blockStartTime, segmentList); + rd.push(block); + } + } + + // Clean up the stack of each row + for (RowData rd : mRowByName.values()) { + Block top = rd.top(); + popFrames(rd, top, Integer.MAX_VALUE, segmentList); + } + + mSurface.setRange(minVal, maxVal); + mSurface.setLimitRange(minVal, maxVal); + + // Sort the rows into decreasing elapsed time + Collection rv = mRowByName.values(); + mRows = rv.toArray(new RowData[rv.size()]); + Arrays.sort(mRows, new Comparator() { + @Override + public int compare(RowData rd1, RowData rd2) { + return (int) (rd2.mElapsed - rd1.mElapsed); + } + }); + + // Assign ranks to the sorted rows + for (int ii = 0; ii < mRows.length; ++ii) { + mRows[ii].mRank = ii; + } + + // Compute the number of rows with data + mNumRows = 0; + for (int ii = 0; ii < mRows.length; ++ii) { + if (mRows[ii].mElapsed == 0) + break; + mNumRows += 1; + } + + // Sort the blocks into increasing rows, and within rows into + // increasing start values. + mSegments = segmentList.toArray(new Segment[segmentList.size()]); + Arrays.sort(mSegments, new Comparator() { + @Override + public int compare(Segment bd1, Segment bd2) { + RowData rd1 = bd1.mRowData; + RowData rd2 = bd2.mRowData; + int diff = rd1.mRank - rd2.mRank; + if (diff == 0) { + long timeDiff = bd1.mStartTime - bd2.mStartTime; + if (timeDiff == 0) + timeDiff = bd1.mEndTime - bd2.mEndTime; + return (int) timeDiff; + } + return diff; + } + }); + + if (false) { + for (Segment segment : mSegments) { + System.out.printf("seg '%s' [%6d, %6d] %s\n", + segment.mRowData.mName, segment.mStartTime, + segment.mEndTime, segment.mBlock.getName()); + if (segment.mStartTime > segment.mEndTime) { + System.err.printf("Error: segment startTime > endTime\n"); + System.exit(1); + } + } + } + } + + private static void popFrames(RowData rd, Block top, long startTime, + ArrayList segmentList) { + long topEndTime = top.getEndTime(); + long lastEndTime = top.getStartTime(); + while (topEndTime <= startTime) { + if (topEndTime > lastEndTime) { + Segment segment = new Segment(rd, top, lastEndTime, topEndTime); + segmentList.add(segment); + lastEndTime = topEndTime; + } + rd.pop(); + top = rd.top(); + if (top == null) + return; + topEndTime = top.getEndTime(); + } + + // If we get here, then topEndTime > startTime + if (lastEndTime < startTime) { + Segment bd = new Segment(rd, top, lastEndTime, startTime); + segmentList.add(bd); + } + } + + private class RowLabels extends Canvas { + + /** The space between the row label and the sash line */ + private static final int labelMarginX = 2; + + public RowLabels(Composite parent) { + super(parent, SWT.NO_BACKGROUND); + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + private void mouseMove(MouseEvent me) { + int rownum = (me.y + mScrollOffsetY) / rowYSpace; + if (mMouseRow != rownum) { + mMouseRow = rownum; + redraw(); + mSurface.redraw(); + } + } + + private void draw(Display display, GC gc) { + if (mSegments.length == 0) { + // gc.setBackground(colorBackground); + // gc.fillRectangle(getBounds()); + return; + } + Point dim = getSize(); + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ + + if (mNumRows > 2) { + // Draw the row background stripes + gcImage.setBackground(mColorRowBack); + for (int ii = 1; ii < mNumRows; ii += 2) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace - mScrollOffsetY; + gcImage.fillRectangle(0, y1, dim.x, rowYSpace); + } + } + + // Draw the row labels + int offsetY = rowYMarginHalf - mScrollOffsetY; + for (int ii = mStartRow; ii <= mEndRow; ++ii) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace + offsetY; + Point extent = gcImage.stringExtent(rd.mName); + int x1 = dim.x - extent.x - labelMarginX; + gcImage.drawString(rd.mName, x1, y1, true); + } + + // Draw a highlight box on the row where the mouse is. + if (mMouseRow >= mStartRow && mMouseRow <= mEndRow) { + gcImage.setForeground(mColorGray); + int y1 = mMouseRow * rowYSpace - mScrollOffsetY; + gcImage.drawRectangle(0, y1, dim.x, rowYSpace); + } + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + } + + private class BlankCorner extends Canvas { + public BlankCorner(Composite parent) { + //super(parent, SWT.NO_BACKGROUND); + super(parent, SWT.NONE); + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + private void draw(Display display, GC gc) { + // Create a blank image and draw it to the canvas + Image image = new Image(display, getBounds()); + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + } + } + + private class Timescale extends Canvas { + private Point mMouse = new Point(LeftMargin, 0); + private Cursor mZoomCursor; + private String mMethodName = null; + private Color mMethodColor = null; + private String mDetails; + private int mMethodStartY; + private int mDetailsStartY; + private int mMarkStartX; + private int mMarkEndX; + + /** The space between the colored block and the method name */ + private static final int METHOD_BLOCK_MARGIN = 10; + + public Timescale(Composite parent) { + //super(parent, SWT.NO_BACKGROUND); + super(parent, SWT.NONE); + Display display = getDisplay(); + mZoomCursor = new Cursor(display, SWT.CURSOR_SIZEWE); + setCursor(mZoomCursor); + mMethodStartY = mSmallFontHeight + 1; + mDetailsStartY = mMethodStartY + mSmallFontHeight + 1; + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + public void setVbarPosition(int x) { + mMouse.x = x; + } + + public void setMarkStart(int x) { + mMarkStartX = x; + } + + public void setMarkEnd(int x) { + mMarkEndX = x; + } + + public void setMethodName(String name) { + mMethodName = name; + } + + public void setMethodColor(Color color) { + mMethodColor = color; + } + + public void setDetails(String details) { + mDetails = details; + } + + private void mouseMove(MouseEvent me) { + me.y = -1; + mSurface.mouseMove(me); + } + + private void mouseDown(MouseEvent me) { + mSurface.startScaling(me.x); + mSurface.redraw(); + } + + private void mouseUp(MouseEvent me) { + mSurface.stopScaling(me.x); + } + + private void mouseDoubleClick(MouseEvent me) { + mSurface.resetScale(); + mSurface.redraw(); + } + + private void draw(Display display, GC gc) { + Point dim = getSize(); + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ + + if (mSurface.drawingSelection()) { + drawSelection(display, gcImage); + } + + drawTicks(display, gcImage); + + // Draw the vertical bar where the mouse is + gcImage.setForeground(mColorDarkGray); + gcImage.drawLine(mMouse.x, timeLineOffsetY, mMouse.x, dim.y); + + // Draw the current millseconds + drawTickLegend(display, gcImage); + + // Draw the method name and color, if needed + drawMethod(display, gcImage); + + // Draw the details, if needed + drawDetails(display, gcImage); + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + + private void drawSelection(Display display, GC gc) { + Point dim = getSize(); + gc.setForeground(mColorGray); + gc.drawLine(mMarkStartX, timeLineOffsetY, mMarkStartX, dim.y); + gc.setBackground(mColorZoomSelection); + int x, width; + if (mMarkStartX < mMarkEndX) { + x = mMarkStartX; + width = mMarkEndX - mMarkStartX; + } else { + x = mMarkEndX; + width = mMarkStartX - mMarkEndX; + } + if (width > 1) { + gc.fillRectangle(x, timeLineOffsetY, width, dim.y); + } + } + + private void drawTickLegend(Display display, GC gc) { + int mouseX = mMouse.x - LeftMargin; + double mouseXval = mScaleInfo.pixelToValue(mouseX); + String info = mUnits.labelledString(mouseXval); + gc.setForeground(mColorForeground); + gc.drawString(info, LeftMargin + 2, 1, true); + + // Display the maximum data value + double maxVal = mScaleInfo.getMaxVal(); + info = mUnits.labelledString(maxVal); + if (mClockSource != null) { + info = String.format(" max %s (%s)", info, mClockSource); //$NON-NLS-1$ + } else { + info = String.format(" max %s ", info); //$NON-NLS-1$ + } + Point extent = gc.stringExtent(info); + Point dim = getSize(); + int x1 = dim.x - RightMargin - extent.x; + gc.drawString(info, x1, 1, true); + } + + private void drawMethod(Display display, GC gc) { + if (mMethodName == null) { + return; + } + + int x1 = LeftMargin; + int y1 = mMethodStartY; + gc.setBackground(mMethodColor); + int width = 2 * mSmallFontWidth; + gc.fillRectangle(x1, y1, width, mSmallFontHeight); + x1 += width + METHOD_BLOCK_MARGIN; + gc.drawString(mMethodName, x1, y1, true); + } + + private void drawDetails(Display display, GC gc) { + if (mDetails == null) { + return; + } + + int x1 = LeftMargin + 2 * mSmallFontWidth + METHOD_BLOCK_MARGIN; + int y1 = mDetailsStartY; + gc.drawString(mDetails, x1, y1, true); + } + + private void drawTicks(Display display, GC gc) { + Point dim = getSize(); + int y2 = majorTickLength + timeLineOffsetY; + int y3 = minorTickLength + timeLineOffsetY; + int y4 = y2 + tickToFontSpacing; + gc.setForeground(mColorForeground); + gc.drawLine(LeftMargin, timeLineOffsetY, dim.x - RightMargin, + timeLineOffsetY); + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double minMajorTick = mScaleInfo.getMinMajorTick(); + double tickIncrement = mScaleInfo.getTickIncrement(); + double minorTickIncrement = tickIncrement / 5; + double pixelsPerRange = mScaleInfo.getPixelsPerRange(); + + // Draw the initial minor ticks, if any + if (minVal < minMajorTick) { + gc.setForeground(mColorGray); + double xMinor = minMajorTick; + for (int ii = 1; ii <= 4; ++ii) { + xMinor -= minorTickIncrement; + if (xMinor < minVal) + break; + int x1 = LeftMargin + + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); + gc.drawLine(x1, timeLineOffsetY, x1, y3); + } + } + + if (tickIncrement <= 10) { + // TODO avoid rendering the loop when tickIncrement is invalid. It can be zero + // or too small. + // System.out.println(String.format("Timescale.drawTicks error: tickIncrement=%1f", tickIncrement)); + return; + } + for (double x = minMajorTick; x <= maxVal; x += tickIncrement) { + int x1 = LeftMargin + + (int) (0.5 + (x - minVal) * pixelsPerRange); + + // Draw a major tick + gc.setForeground(mColorForeground); + gc.drawLine(x1, timeLineOffsetY, x1, y2); + if (x > maxVal) + break; + + // Draw the tick text + String tickString = mUnits.valueOf(x); + gc.drawString(tickString, x1, y4, true); + + // Draw 4 minor ticks between major ticks + gc.setForeground(mColorGray); + double xMinor = x; + for (int ii = 1; ii <= 4; ii++) { + xMinor += minorTickIncrement; + if (xMinor > maxVal) + break; + x1 = LeftMargin + + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); + gc.drawLine(x1, timeLineOffsetY, x1, y3); + } + } + } + } + + private static enum GraphicsState { + Normal, Marking, Scaling, Animating, Scrolling + }; + + private class Surface extends Canvas { + + public Surface(Composite parent) { + super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL | SWT.H_SCROLL); + Display display = getDisplay(); + mNormalCursor = new Cursor(display, SWT.CURSOR_CROSS); + mIncreasingCursor = new Cursor(display, SWT.CURSOR_SIZEE); + mDecreasingCursor = new Cursor(display, SWT.CURSOR_SIZEW); + + initZoomFractionsWithExp(); + + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + + mZoomAnimator = new Runnable() { + @Override + public void run() { + animateZoom(); + } + }; + + mHighlightAnimator = new Runnable() { + @Override + public void run() { + animateHighlight(); + } + }; + } + + private void initZoomFractionsWithExp() { + mZoomFractions = new double[ZOOM_STEPS]; + int next = 0; + for (int ii = 0; ii < ZOOM_STEPS / 2; ++ii, ++next) { + mZoomFractions[next] = (double) (1 << ii) + / (double) (1 << (ZOOM_STEPS / 2)); + // System.out.printf("%d %f\n", next, zoomFractions[next]); + } + for (int ii = 2; ii < 2 + ZOOM_STEPS / 2; ++ii, ++next) { + mZoomFractions[next] = (double) ((1 << ii) - 1) + / (double) (1 << ii); + // System.out.printf("%d %f\n", next, zoomFractions[next]); + } + } + + @SuppressWarnings("unused") + private void initZoomFractionsWithSinWave() { + mZoomFractions = new double[ZOOM_STEPS]; + for (int ii = 0; ii < ZOOM_STEPS; ++ii) { + double offset = Math.PI * ii / ZOOM_STEPS; + mZoomFractions[ii] = (Math.sin((1.5 * Math.PI + offset)) + 1.0) / 2.0; + // System.out.printf("%d %f\n", ii, zoomFractions[ii]); + } + } + + public void setRange(double minVal, double maxVal) { + mMinDataVal = minVal; + mMaxDataVal = maxVal; + mScaleInfo.setMinVal(minVal); + mScaleInfo.setMaxVal(maxVal); + } + + public void setLimitRange(double minVal, double maxVal) { + mLimitMinVal = minVal; + mLimitMaxVal = maxVal; + } + + public void resetScale() { + mScaleInfo.setMinVal(mLimitMinVal); + mScaleInfo.setMaxVal(mLimitMaxVal); + } + + public void setScaleFromHorizontalScrollBar(int selection) { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double visibleRange = maxVal - minVal; + + minVal = mLimitMinVal + selection; + maxVal = minVal + visibleRange; + if (maxVal > mLimitMaxVal) { + maxVal = mLimitMaxVal; + minVal = maxVal - visibleRange; + } + mScaleInfo.setMinVal(minVal); + mScaleInfo.setMaxVal(maxVal); + + mGraphicsState = GraphicsState.Scrolling; + } + + private void updateHorizontalScrollBar() { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double visibleRange = maxVal - minVal; + double fullRange = mLimitMaxVal - mLimitMinVal; + + ScrollBar hBar = getHorizontalBar(); + if (fullRange > visibleRange) { + hBar.setVisible(true); + hBar.setMinimum(0); + hBar.setMaximum((int)Math.ceil(fullRange)); + hBar.setThumb((int)Math.ceil(visibleRange)); + hBar.setSelection((int)Math.floor(minVal - mLimitMinVal)); + } else { + hBar.setVisible(false); + } + } + + private void draw(Display display, GC gc) { + if (mSegments.length == 0) { + // gc.setBackground(colorBackground); + // gc.fillRectangle(getBounds()); + return; + } + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ + + // Draw the background + // gcImage.setBackground(colorBackground); + // gcImage.fillRectangle(image.getBounds()); + + if (mGraphicsState == GraphicsState.Scaling) { + double diff = mMouse.x - mMouseMarkStartX; + if (diff > 0) { + double newMinVal = mScaleMinVal - diff / mScalePixelsPerRange; + if (newMinVal < mLimitMinVal) + newMinVal = mLimitMinVal; + mScaleInfo.setMinVal(newMinVal); + // System.out.printf("diff %f scaleMin %f newMin %f\n", + // diff, scaleMinVal, newMinVal); + } else if (diff < 0) { + double newMaxVal = mScaleMaxVal - diff / mScalePixelsPerRange; + if (newMaxVal > mLimitMaxVal) + newMaxVal = mLimitMaxVal; + mScaleInfo.setMaxVal(newMaxVal); + // System.out.printf("diff %f scaleMax %f newMax %f\n", + // diff, scaleMaxVal, newMaxVal); + } + } + + // Recompute the ticks and strips only if the size has changed, + // or we scrolled so that a new row is visible. + Point dim = getSize(); + if (mStartRow != mCachedStartRow || mEndRow != mCachedEndRow + || mScaleInfo.getMinVal() != mCachedMinVal + || mScaleInfo.getMaxVal() != mCachedMaxVal) { + mCachedStartRow = mStartRow; + mCachedEndRow = mEndRow; + int xdim = dim.x - TotalXMargin; + mScaleInfo.setNumPixels(xdim); + boolean forceEndPoints = (mGraphicsState == GraphicsState.Scaling + || mGraphicsState == GraphicsState.Animating + || mGraphicsState == GraphicsState.Scrolling); + mScaleInfo.computeTicks(forceEndPoints); + mCachedMinVal = mScaleInfo.getMinVal(); + mCachedMaxVal = mScaleInfo.getMaxVal(); + if (mLimitMinVal > mScaleInfo.getMinVal()) + mLimitMinVal = mScaleInfo.getMinVal(); + if (mLimitMaxVal < mScaleInfo.getMaxVal()) + mLimitMaxVal = mScaleInfo.getMaxVal(); + + // Compute the strips + computeStrips(); + + // Update the horizontal scrollbar. + updateHorizontalScrollBar(); + } + + if (mNumRows > 2) { + // Draw the row background stripes + gcImage.setBackground(mColorRowBack); + for (int ii = 1; ii < mNumRows; ii += 2) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace - mScrollOffsetY; + gcImage.fillRectangle(0, y1, dim.x, rowYSpace); + } + } + + if (drawingSelection()) { + drawSelection(display, gcImage); + } + + String blockName = null; + Color blockColor = null; + String blockDetails = null; + + if (mDebug) { + double pixelsPerRange = mScaleInfo.getPixelsPerRange(); + System.out + .printf( + "dim.x %d pixels %d minVal %f, maxVal %f ppr %f rpp %f\n", + dim.x, dim.x - TotalXMargin, mScaleInfo + .getMinVal(), mScaleInfo.getMaxVal(), + pixelsPerRange, 1.0 / pixelsPerRange); + } + + // Draw the strips + Block selectBlock = null; + for (Strip strip : mStripList) { + if (strip.mColor == null) { + // System.out.printf("strip.color is null\n"); + continue; + } + gcImage.setBackground(strip.mColor); + gcImage.fillRectangle(strip.mX, strip.mY - mScrollOffsetY, strip.mWidth, + strip.mHeight); + if (mMouseRow == strip.mRowData.mRank) { + if (mMouse.x >= strip.mX + && mMouse.x < strip.mX + strip.mWidth) { + Block block = strip.mSegment.mBlock; + blockName = block.getName(); + blockColor = strip.mColor; + if (mHaveCpuTime) { + if (mHaveRealTime) { + blockDetails = String.format( + "excl cpu %s, incl cpu %s, " + + "excl real %s, incl real %s", + mUnits.labelledString(block.getExclusiveCpuTime()), + mUnits.labelledString(block.getInclusiveCpuTime()), + mUnits.labelledString(block.getExclusiveRealTime()), + mUnits.labelledString(block.getInclusiveRealTime())); + } else { + blockDetails = String.format( + "excl cpu %s, incl cpu %s", + mUnits.labelledString(block.getExclusiveCpuTime()), + mUnits.labelledString(block.getInclusiveCpuTime())); + } + } else { + blockDetails = String.format( + "excl real %s, incl real %s", + mUnits.labelledString(block.getExclusiveRealTime()), + mUnits.labelledString(block.getInclusiveRealTime())); + } + } + if (mMouseSelect.x >= strip.mX + && mMouseSelect.x < strip.mX + strip.mWidth) { + selectBlock = strip.mSegment.mBlock; + } + } + } + mMouseSelect.x = 0; + mMouseSelect.y = 0; + + if (selectBlock != null) { + ArrayList selections = new ArrayList(); + // Get the row label + RowData rd = mRows[mMouseRow]; + selections.add(Selection.highlight("Thread", rd.mName)); //$NON-NLS-1$ + selections.add(Selection.highlight("Call", selectBlock)); //$NON-NLS-1$ + + int mouseX = mMouse.x - LeftMargin; + double mouseXval = mScaleInfo.pixelToValue(mouseX); + selections.add(Selection.highlight("Time", mouseXval)); //$NON-NLS-1$ + + mSelectionController.change(selections, "TimeLineView"); //$NON-NLS-1$ + mHighlightMethodData = null; + mHighlightCall = (Call) selectBlock; + startHighlighting(); + } + + // Draw a highlight box on the row where the mouse is. + // Except don't draw the box if we are animating the + // highlighing of a call or method because the inclusive + // highlight bar passes through the highlight box and + // causes an annoying flashing artifact. + if (mMouseRow >= 0 && mMouseRow < mNumRows && mHighlightStep == 0) { + gcImage.setForeground(mColorGray); + int y1 = mMouseRow * rowYSpace - mScrollOffsetY; + gcImage.drawLine(0, y1, dim.x, y1); + gcImage.drawLine(0, y1 + rowYSpace, dim.x, y1 + rowYSpace); + } + + // Highlight a selected method, if any + drawHighlights(gcImage, dim); + + // Draw a vertical line where the mouse is. + gcImage.setForeground(mColorDarkGray); + int lineEnd = Math.min(dim.y, mNumRows * rowYSpace); + gcImage.drawLine(mMouse.x, 0, mMouse.x, lineEnd); + + if (blockName != null) { + mTimescale.setMethodName(blockName); + mTimescale.setMethodColor(blockColor); + mTimescale.setDetails(blockDetails); + mShowHighlightName = false; + } else if (mShowHighlightName) { + // Draw the highlighted method name + MethodData md = mHighlightMethodData; + if (md == null && mHighlightCall != null) + md = mHighlightCall.getMethodData(); + if (md == null) + System.out.printf("null highlight?\n"); //$NON-NLS-1$ + if (md != null) { + mTimescale.setMethodName(md.getProfileName()); + mTimescale.setMethodColor(md.getColor()); + mTimescale.setDetails(null); + } + } else { + mTimescale.setMethodName(null); + mTimescale.setMethodColor(null); + mTimescale.setDetails(null); + } + mTimescale.redraw(); + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + + private void drawHighlights(GC gc, Point dim) { + int height = mHighlightHeight; + if (height <= 0) + return; + for (Range range : mHighlightExclusive) { + gc.setBackground(range.mColor); + int xStart = range.mXdim.x; + int width = range.mXdim.y; + gc.fillRectangle(xStart, range.mY - height - mScrollOffsetY, width, height); + } + + // Draw the inclusive lines a bit shorter + height -= 1; + if (height <= 0) + height = 1; + + // Highlight the inclusive ranges + gc.setForeground(mColorDarkGray); + gc.setBackground(mColorDarkGray); + for (Range range : mHighlightInclusive) { + int x1 = range.mXdim.x; + int x2 = range.mXdim.y; + boolean drawLeftEnd = false; + boolean drawRightEnd = false; + if (x1 >= LeftMargin) + drawLeftEnd = true; + else + x1 = LeftMargin; + if (x2 >= LeftMargin) + drawRightEnd = true; + else + x2 = dim.x - RightMargin; + int y1 = range.mY + rowHeight + 2 - mScrollOffsetY; + + // If the range is very narrow, then just draw a small + // rectangle. + if (x2 - x1 < MinInclusiveRange) { + int width = x2 - x1; + if (width < 2) + width = 2; + gc.fillRectangle(x1, y1, width, height); + continue; + } + if (drawLeftEnd) { + if (drawRightEnd) { + // Draw both ends + int[] points = { x1, y1, x1, y1 + height, x2, + y1 + height, x2, y1 }; + gc.drawPolyline(points); + } else { + // Draw the left end + int[] points = { x1, y1, x1, y1 + height, x2, + y1 + height }; + gc.drawPolyline(points); + } + } else { + if (drawRightEnd) { + // Draw the right end + int[] points = { x1, y1 + height, x2, y1 + height, x2, + y1 }; + gc.drawPolyline(points); + } else { + // Draw neither end, just the line + int[] points = { x1, y1 + height, x2, y1 + height }; + gc.drawPolyline(points); + } + } + + // Draw the arrowheads, if necessary + if (drawLeftEnd == false) { + int[] points = { x1 + 7, y1 + height - 4, x1, y1 + height, + x1 + 7, y1 + height + 4 }; + gc.fillPolygon(points); + } + if (drawRightEnd == false) { + int[] points = { x2 - 7, y1 + height - 4, x2, y1 + height, + x2 - 7, y1 + height + 4 }; + gc.fillPolygon(points); + } + } + } + + private boolean drawingSelection() { + return mGraphicsState == GraphicsState.Marking + || mGraphicsState == GraphicsState.Animating; + } + + private void drawSelection(Display display, GC gc) { + Point dim = getSize(); + gc.setForeground(mColorGray); + gc.drawLine(mMouseMarkStartX, 0, mMouseMarkStartX, dim.y); + gc.setBackground(mColorZoomSelection); + int width; + int mouseX = (mGraphicsState == GraphicsState.Animating) ? mMouseMarkEndX : mMouse.x; + int x; + if (mMouseMarkStartX < mouseX) { + x = mMouseMarkStartX; + width = mouseX - mMouseMarkStartX; + } else { + x = mouseX; + width = mMouseMarkStartX - mouseX; + } + gc.fillRectangle(x, 0, width, dim.y); + } + + private void computeStrips() { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + + // Allocate space for the pixel data + Pixel[] pixels = new Pixel[mNumRows]; + for (int ii = 0; ii < mNumRows; ++ii) + pixels[ii] = new Pixel(); + + // Clear the per-block pixel data + for (int ii = 0; ii < mSegments.length; ++ii) { + mSegments[ii].mBlock.clearWeight(); + } + + mStripList.clear(); + mHighlightExclusive.clear(); + mHighlightInclusive.clear(); + MethodData callMethod = null; + long callStart = 0; + long callEnd = -1; + RowData callRowData = null; + int prevMethodStart = -1; + int prevMethodEnd = -1; + int prevCallStart = -1; + int prevCallEnd = -1; + if (mHighlightCall != null) { + int callPixelStart = -1; + int callPixelEnd = -1; + callStart = mHighlightCall.getStartTime(); + callEnd = mHighlightCall.getEndTime(); + callMethod = mHighlightCall.getMethodData(); + if (callStart >= minVal) + callPixelStart = mScaleInfo.valueToPixel(callStart); + if (callEnd <= maxVal) + callPixelEnd = mScaleInfo.valueToPixel(callEnd); + // System.out.printf("callStart,End %d,%d minVal,maxVal %f,%f + // callPixelStart,End %d,%d\n", + // callStart, callEnd, minVal, maxVal, callPixelStart, + // callPixelEnd); + int threadId = mHighlightCall.getThreadId(); + String threadName = mThreadLabels.get(threadId); + callRowData = mRowByName.get(threadName); + int y1 = callRowData.mRank * rowYSpace + rowYMarginHalf; + Color color = callMethod.getColor(); + mHighlightInclusive.add(new Range(callPixelStart + LeftMargin, + callPixelEnd + LeftMargin, y1, color)); + } + for (Segment segment : mSegments) { + if (segment.mEndTime <= minVal) + continue; + if (segment.mStartTime >= maxVal) + continue; + + Block block = segment.mBlock; + + // Skip over blocks that were not assigned a color, including the + // top level block and others that have zero inclusive time. + Color color = block.getColor(); + if (color == null) + continue; + + double recordStart = Math.max(segment.mStartTime, minVal); + double recordEnd = Math.min(segment.mEndTime, maxVal); + if (recordStart == recordEnd) + continue; + int pixelStart = mScaleInfo.valueToPixel(recordStart); + int pixelEnd = mScaleInfo.valueToPixel(recordEnd); + int width = pixelEnd - pixelStart; + boolean isContextSwitch = segment.mIsContextSwitch; + + RowData rd = segment.mRowData; + MethodData md = block.getMethodData(); + + // We will add the scroll offset later when we draw the strips + int y1 = rd.mRank * rowYSpace + rowYMarginHalf; + + // If we can't display any more rows, then quit + if (rd.mRank > mEndRow) + break; + + // System.out.printf("segment %s val: [%.1f, %.1f] frac [%f, %f] + // pixel: [%d, %d] pix.start %d weight %.2f %s\n", + // block.getName(), recordStart, recordEnd, + // scaleInfo.valueToPixelFraction(recordStart), + // scaleInfo.valueToPixelFraction(recordEnd), + // pixelStart, pixelEnd, pixels[rd.rank].start, + // pixels[rd.rank].maxWeight, + // pixels[rd.rank].segment != null + // ? pixels[rd.rank].segment.block.getName() + // : "null"); + + if (mHighlightMethodData != null) { + if (mHighlightMethodData == md) { + if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { + prevMethodStart = pixelStart; + prevMethodEnd = pixelEnd; + int rangeWidth = width; + if (rangeWidth == 0) + rangeWidth = 1; + mHighlightExclusive.add(new Range(pixelStart + + LeftMargin, rangeWidth, y1, color)); + callStart = block.getStartTime(); + int callPixelStart = -1; + if (callStart >= minVal) + callPixelStart = mScaleInfo.valueToPixel(callStart); + int callPixelEnd = -1; + callEnd = block.getEndTime(); + if (callEnd <= maxVal) + callPixelEnd = mScaleInfo.valueToPixel(callEnd); + if (prevCallStart != callPixelStart || prevCallEnd != callPixelEnd) { + prevCallStart = callPixelStart; + prevCallEnd = callPixelEnd; + mHighlightInclusive.add(new Range( + callPixelStart + LeftMargin, + callPixelEnd + LeftMargin, y1, color)); + } + } + } else if (mFadeColors) { + color = md.getFadedColor(); + } + } else if (mHighlightCall != null) { + if (segment.mStartTime >= callStart + && segment.mEndTime <= callEnd && callMethod == md + && callRowData == rd) { + if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { + prevMethodStart = pixelStart; + prevMethodEnd = pixelEnd; + int rangeWidth = width; + if (rangeWidth == 0) + rangeWidth = 1; + mHighlightExclusive.add(new Range(pixelStart + + LeftMargin, rangeWidth, y1, color)); + } + } else if (mFadeColors) { + color = md.getFadedColor(); + } + } + + // Cases: + // 1. This segment starts on a different pixel than the + // previous segment started on. In this case, emit + // the pixel strip, if any, and: + // A. If the width is 0, then add this segment's + // weight to the Pixel. + // B. If the width > 0, then emit a strip for this + // segment (no partial Pixel data). + // + // 2. Otherwise (the new segment starts on the same + // pixel as the previous segment): add its "weight" + // to the current pixel, and: + // A. If the new segment has width 1, + // then emit the pixel strip and then + // add the segment's weight to the pixel. + // B. If the new segment has width > 1, + // then emit the pixel strip, and emit the rest + // of the strip for this segment (no partial Pixel + // data). + + Pixel pix = pixels[rd.mRank]; + if (pix.mStart != pixelStart) { + if (pix.mSegment != null) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + } + + if (width == 0) { + // Compute the "weight" of this segment for the first + // pixel. For a pixel N, the "weight" of a segment is + // how much of the region [N - 0.5, N + 0.5] is covered + // by the segment. + double weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + if (weight > pix.mMaxWeight) { + pix.setFields(pixelStart, weight, segment, color, + rd); + } + } else { + int x1 = pixelStart + LeftMargin; + Strip strip = new Strip( + x1, isContextSwitch ? y1 + rowHeight - 1 : y1, + width, isContextSwitch ? 1 : rowHeight, + rd, segment, color); + mStripList.add(strip); + } + } else { + double weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + if (weight > pix.mMaxWeight) { + pix.setFields(pixelStart, weight, segment, color, rd); + } + if (width == 1) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + + // Compute the weight for the next pixel + pixelStart += 1; + weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + pix.setFields(pixelStart, weight, segment, color, rd); + } else if (width > 1) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + + // Emit a strip for the rest of the segment. + pixelStart += 1; + width -= 1; + int x1 = pixelStart + LeftMargin; + Strip strip = new Strip( + x1, isContextSwitch ? y1 + rowHeight - 1 : y1, + width, isContextSwitch ? 1 : rowHeight, + rd,segment, color); + mStripList.add(strip); + } + } + } + + // Emit the last pixels of each row, if any + for (int ii = 0; ii < mNumRows; ++ii) { + Pixel pix = pixels[ii]; + if (pix.mSegment != null) { + RowData rd = pix.mRowData; + int y1 = rd.mRank * rowYSpace + rowYMarginHalf; + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + } + } + + if (false) { + System.out.printf("computeStrips()\n"); + for (Strip strip : mStripList) { + System.out.printf("%3d, %3d width %3d height %d %s\n", + strip.mX, strip.mY, strip.mWidth, strip.mHeight, + strip.mSegment.mBlock.getName()); + } + } + } + + private double computeWeight(double start, double end, + boolean isContextSwitch, int pixel) { + if (isContextSwitch) { + return 0; + } + double pixelStartFraction = mScaleInfo.valueToPixelFraction(start); + double pixelEndFraction = mScaleInfo.valueToPixelFraction(end); + double leftEndPoint = Math.max(pixelStartFraction, pixel - 0.5); + double rightEndPoint = Math.min(pixelEndFraction, pixel + 0.5); + double weight = rightEndPoint - leftEndPoint; + return weight; + } + + private void emitPixelStrip(RowData rd, int y, Pixel pixel) { + Strip strip; + + if (pixel.mSegment == null) + return; + + int x = pixel.mStart + LeftMargin; + // Compute the percentage of the row height proportional to + // the weight of this pixel. But don't let the proportion + // exceed 3/4 of the row height so that we can easily see + // if a given time range includes more than one method. + int height = (int) (pixel.mMaxWeight * rowHeight * 0.75); + if (height < mMinStripHeight) + height = mMinStripHeight; + int remainder = rowHeight - height; + if (remainder > 0) { + strip = new Strip(x, y, 1, remainder, rd, pixel.mSegment, + mFadeColors ? mColorGray : mColorBlack); + mStripList.add(strip); + // System.out.printf("emitPixel (%d, %d) height %d black\n", + // x, y, remainder); + } + strip = new Strip(x, y + remainder, 1, height, rd, pixel.mSegment, + pixel.mColor); + mStripList.add(strip); + // System.out.printf("emitPixel (%d, %d) height %d %s\n", + // x, y + remainder, height, pixel.segment.block.getName()); + pixel.mSegment = null; + pixel.mMaxWeight = 0.0; + } + + private void mouseMove(MouseEvent me) { + if (false) { + if (mHighlightMethodData != null) { + mHighlightMethodData = null; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + } + } + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouse.x = x; + mMouse.y = me.y; + mTimescale.setVbarPosition(x); + if (mGraphicsState == GraphicsState.Marking) { + mTimescale.setMarkEnd(x); + } + + if (mGraphicsState == GraphicsState.Normal) { + // Set the cursor to the normal state. + mSurface.setCursor(mNormalCursor); + } else if (mGraphicsState == GraphicsState.Marking) { + // Make the cursor point in the direction of the sweep + if (mMouse.x >= mMouseMarkStartX) + mSurface.setCursor(mIncreasingCursor); + else + mSurface.setCursor(mDecreasingCursor); + } + int rownum = (mMouse.y + mScrollOffsetY) / rowYSpace; + if (me.y < 0 || me.y >= dim.y) { + rownum = -1; + } + if (mMouseRow != rownum) { + mMouseRow = rownum; + mLabels.redraw(); + } + redraw(); + } + + private void mouseDown(MouseEvent me) { + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkStartX = x; + mGraphicsState = GraphicsState.Marking; + mSurface.setCursor(mIncreasingCursor); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkStartX); + redraw(); + } + + private void mouseUp(MouseEvent me) { + mSurface.setCursor(mNormalCursor); + if (mGraphicsState != GraphicsState.Marking) { + mGraphicsState = GraphicsState.Normal; + return; + } + mGraphicsState = GraphicsState.Animating; + Point dim = mSurface.getSize(); + + // If the user released the mouse outside the drawing area then + // cancel the zoom. + if (me.y <= 0 || me.y >= dim.y) { + mGraphicsState = GraphicsState.Normal; + redraw(); + return; + } + + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkEndX = x; + + // If the user clicked and released the mouse at the same point + // (+/- a pixel or two) then cancel the zoom (but select the + // method). + int dist = mMouseMarkEndX - mMouseMarkStartX; + if (dist < 0) + dist = -dist; + if (dist <= 2) { + mGraphicsState = GraphicsState.Normal; + + // Select the method underneath the mouse + mMouseSelect.x = mMouseMarkStartX; + mMouseSelect.y = me.y; + redraw(); + return; + } + + // Make mouseEndX be the higher end point + if (mMouseMarkEndX < mMouseMarkStartX) { + int temp = mMouseMarkEndX; + mMouseMarkEndX = mMouseMarkStartX; + mMouseMarkStartX = temp; + } + + // If the zoom area is the whole window (or nearly the whole + // window) then cancel the zoom. + if (mMouseMarkStartX <= LeftMargin + MinZoomPixelMargin + && mMouseMarkEndX >= dim.x - RightMargin - MinZoomPixelMargin) { + mGraphicsState = GraphicsState.Normal; + redraw(); + return; + } + + // Compute some variables needed for zooming. + // It's probably easiest to explain by an example. There + // are two scales (or dimensions) involved: one for the pixels + // and one for the values (microseconds). To keep the example + // simple, suppose we have pixels in the range [0,16] and + // values in the range [100, 260], and suppose the user + // selects a zoom window from pixel 4 to pixel 8. + // + // usec: 100 140 180 260 + // |-------|ZZZZZZZ|---------------| + // pixel: 0 4 8 16 + // + // I've drawn the pixels starting at zero for simplicity, but + // in fact the drawable area is offset from the left margin + // by the value of "LeftMargin". + // + // The "pixels-per-range" (ppr) in this case is 0.1 (a tenth of + // a pixel per usec). What we want is to redraw the screen in + // several steps, each time increasing the zoom window until the + // zoom window fills the screen. For simplicity, assume that + // we want to zoom in four equal steps. Then the snapshots + // of the screen at each step would look something like this: + // + // usec: 100 140 180 260 + // |-------|ZZZZZZZ|---------------| + // pixel: 0 4 8 16 + // + // usec: ? 140 180 ? + // |-----|ZZZZZZZZZZZZZ|-----------| + // pixel: 0 3 10 16 + // + // usec: ? 140 180 ? + // |---|ZZZZZZZZZZZZZZZZZZZ|-------| + // pixel: 0 2 12 16 + // + // usec: ?140 180 ? + // |-|ZZZZZZZZZZZZZZZZZZZZZZZZZ|---| + // pixel: 0 1 14 16 + // + // usec: 140 180 + // |ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ| + // pixel: 0 16 + // + // The problem is how to compute the endpoints (denoted by ?) + // for each step. This is a little tricky. We first need to + // compute the "fixed point": this is the point in the selection + // that doesn't move left or right. Then we can recompute the + // "ppr" (pixels per range) at each step and then find the + // endpoints. The computation of the end points is done + // in animateZoom(). This method computes the fixed point + // and some other variables needed in animateZoom(). + + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double ppr = mScaleInfo.getPixelsPerRange(); + mZoomMin = minVal + ((mMouseMarkStartX - LeftMargin) / ppr); + mZoomMax = minVal + ((mMouseMarkEndX - LeftMargin) / ppr); + + // Clamp the min and max values to the actual data min and max + if (mZoomMin < mMinDataVal) + mZoomMin = mMinDataVal; + if (mZoomMax > mMaxDataVal) + mZoomMax = mMaxDataVal; + + // Snap the min and max points to the grid determined by the + // TickScaler + // before we zoom. + int xdim = dim.x - TotalXMargin; + TickScaler scaler = new TickScaler(mZoomMin, mZoomMax, xdim, + PixelsPerTick); + scaler.computeTicks(false); + mZoomMin = scaler.getMinVal(); + mZoomMax = scaler.getMaxVal(); + + // Also snap the mouse points (in pixel space) to be consistent with + // zoomMin and zoomMax (in value space). + mMouseMarkStartX = (int) ((mZoomMin - minVal) * ppr + LeftMargin); + mMouseMarkEndX = (int) ((mZoomMax - minVal) * ppr + LeftMargin); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + + // Compute the mouse selection end point distances + mMouseEndDistance = dim.x - RightMargin - mMouseMarkEndX; + mMouseStartDistance = mMouseMarkStartX - LeftMargin; + mZoomMouseStart = mMouseMarkStartX; + mZoomMouseEnd = mMouseMarkEndX; + mZoomStep = 0; + + // Compute the fixed point in both value space and pixel space. + mMin2ZoomMin = mZoomMin - minVal; + mZoomMax2Max = maxVal - mZoomMax; + mZoomFixed = mZoomMin + (mZoomMax - mZoomMin) * mMin2ZoomMin + / (mMin2ZoomMin + mZoomMax2Max); + mZoomFixedPixel = (mZoomFixed - minVal) * ppr + LeftMargin; + mFixedPixelStartDistance = mZoomFixedPixel - LeftMargin; + mFixedPixelEndDistance = dim.x - RightMargin - mZoomFixedPixel; + + mZoomMin2Fixed = mZoomFixed - mZoomMin; + mFixed2ZoomMax = mZoomMax - mZoomFixed; + + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + redraw(); + update(); + } + + private void mouseScrolled(MouseEvent me) { + mGraphicsState = GraphicsState.Scrolling; + double tMin = mScaleInfo.getMinVal(); + double tMax = mScaleInfo.getMaxVal(); + double zoomFactor = 2; + double tMinRef = mLimitMinVal; + double tMaxRef = mLimitMaxVal; + double t; // the fixed point + double tMinNew; + double tMaxNew; + if (me.count > 0) { + // we zoom in + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + double ppr = mScaleInfo.getPixelsPerRange(); + t = tMin + ((x - LeftMargin) / ppr); + tMinNew = Math.max(tMinRef, t - (t - tMin) / zoomFactor); + tMaxNew = Math.min(tMaxRef, t + (tMax - t) / zoomFactor); + } else { + // we zoom out + double factor = (tMax - tMin) / (tMaxRef - tMinRef); + if (factor < 1) { + t = (factor * tMinRef - tMin) / (factor - 1); + tMinNew = Math.max(tMinRef, t - zoomFactor * (t - tMin)); + tMaxNew = Math.min(tMaxRef, t + zoomFactor * (tMax - t)); + } else { + return; + } + } + mScaleInfo.setMinVal(tMinNew); + mScaleInfo.setMaxVal(tMaxNew); + mSurface.redraw(); + } + + // No defined behavior yet for double-click. + private void mouseDoubleClick(MouseEvent me) { + } + + public void startScaling(int mouseX) { + Point dim = mSurface.getSize(); + int x = mouseX; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkStartX = x; + mGraphicsState = GraphicsState.Scaling; + mScalePixelsPerRange = mScaleInfo.getPixelsPerRange(); + mScaleMinVal = mScaleInfo.getMinVal(); + mScaleMaxVal = mScaleInfo.getMaxVal(); + } + + public void stopScaling(int mouseX) { + mGraphicsState = GraphicsState.Normal; + } + + private void animateHighlight() { + mHighlightStep += 1; + if (mHighlightStep >= HIGHLIGHT_STEPS) { + mFadeColors = false; + mHighlightStep = 0; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + } else { + mFadeColors = true; + mShowHighlightName = true; + mHighlightHeight = highlightHeights[mHighlightStep]; + getDisplay().timerExec(HIGHLIGHT_TIMER_INTERVAL, mHighlightAnimator); + } + redraw(); + } + + private void clearHighlights() { + // System.out.printf("clearHighlights()\n"); + mShowHighlightName = false; + mHighlightHeight = 0; + mHighlightMethodData = null; + mHighlightCall = null; + mFadeColors = false; + mHighlightStep = 0; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + redraw(); + } + + private void animateZoom() { + mZoomStep += 1; + if (mZoomStep > ZOOM_STEPS) { + mGraphicsState = GraphicsState.Normal; + // Force a normal recomputation + mCachedMinVal = mScaleInfo.getMinVal() + 1; + } else if (mZoomStep == ZOOM_STEPS) { + mScaleInfo.setMinVal(mZoomMin); + mScaleInfo.setMaxVal(mZoomMax); + mMouseMarkStartX = LeftMargin; + Point dim = getSize(); + mMouseMarkEndX = dim.x - RightMargin; + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + } else { + // Zoom in slowly at first, then speed up, then slow down. + // The zoom fractions are precomputed to save time. + double fraction = mZoomFractions[mZoomStep]; + mMouseMarkStartX = (int) (mZoomMouseStart - fraction * mMouseStartDistance); + mMouseMarkEndX = (int) (mZoomMouseEnd + fraction * mMouseEndDistance); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + + // Compute the new pixels-per-range. Avoid division by zero. + double ppr; + if (mZoomMin2Fixed >= mFixed2ZoomMax) + ppr = (mZoomFixedPixel - mMouseMarkStartX) / mZoomMin2Fixed; + else + ppr = (mMouseMarkEndX - mZoomFixedPixel) / mFixed2ZoomMax; + double newMin = mZoomFixed - mFixedPixelStartDistance / ppr; + double newMax = mZoomFixed + mFixedPixelEndDistance / ppr; + mScaleInfo.setMinVal(newMin); + mScaleInfo.setMaxVal(newMax); + + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + } + redraw(); + } + + private static final int TotalXMargin = LeftMargin + RightMargin; + private static final int yMargin = 1; // blank space on top + // The minimum margin on each side of the zoom window, in pixels. + private static final int MinZoomPixelMargin = 10; + private GraphicsState mGraphicsState = GraphicsState.Normal; + private Point mMouse = new Point(LeftMargin, 0); + private int mMouseMarkStartX; + private int mMouseMarkEndX; + private boolean mDebug = false; + private ArrayList mStripList = new ArrayList(); + private ArrayList mHighlightExclusive = new ArrayList(); + private ArrayList mHighlightInclusive = new ArrayList(); + private int mMinStripHeight = 2; + private double mCachedMinVal; + private double mCachedMaxVal; + private int mCachedStartRow; + private int mCachedEndRow; + private double mScalePixelsPerRange; + private double mScaleMinVal; + private double mScaleMaxVal; + private double mLimitMinVal; + private double mLimitMaxVal; + private double mMinDataVal; + private double mMaxDataVal; + private Cursor mNormalCursor; + private Cursor mIncreasingCursor; + private Cursor mDecreasingCursor; + private static final int ZOOM_TIMER_INTERVAL = 10; + private static final int HIGHLIGHT_TIMER_INTERVAL = 50; + private static final int ZOOM_STEPS = 8; // must be even + private int mHighlightHeight = 4; + private final int[] highlightHeights = { 0, 2, 4, 5, 6, 5, 4, 2, 4, 5, + 6 }; + private final int HIGHLIGHT_STEPS = highlightHeights.length; + private boolean mFadeColors; + private boolean mShowHighlightName; + private double[] mZoomFractions; + private int mZoomStep; + private int mZoomMouseStart; + private int mZoomMouseEnd; + private int mMouseStartDistance; + private int mMouseEndDistance; + private Point mMouseSelect = new Point(0, 0); + private double mZoomFixed; + private double mZoomFixedPixel; + private double mFixedPixelStartDistance; + private double mFixedPixelEndDistance; + private double mZoomMin2Fixed; + private double mMin2ZoomMin; + private double mFixed2ZoomMax; + private double mZoomMax2Max; + private double mZoomMin; + private double mZoomMax; + private Runnable mZoomAnimator; + private Runnable mHighlightAnimator; + private int mHighlightStep; + } + + private int computeVisibleRows(int ydim) { + // If we resize, then move the bottom row down. Don't allow the scroll + // to waste space at the bottom. + int offsetY = mScrollOffsetY; + int spaceNeeded = mNumRows * rowYSpace; + if (offsetY + ydim > spaceNeeded) { + offsetY = spaceNeeded - ydim; + if (offsetY < 0) { + offsetY = 0; + } + } + mStartRow = offsetY / rowYSpace; + mEndRow = (offsetY + ydim) / rowYSpace; + if (mEndRow >= mNumRows) { + mEndRow = mNumRows - 1; + } + + return offsetY; + } + + private void startHighlighting() { + // System.out.printf("startHighlighting()\n"); + mSurface.mHighlightStep = 0; + mSurface.mFadeColors = true; + // Force a recomputation of the color strips + mSurface.mCachedEndRow = -1; + getDisplay().timerExec(0, mSurface.mHighlightAnimator); + } + + private static class RowData { + RowData(Row row) { + mName = row.getName(); + mStack = new ArrayList(); + } + + public void push(Block block) { + mStack.add(block); + } + + public Block top() { + if (mStack.size() == 0) + return null; + return mStack.get(mStack.size() - 1); + } + + public void pop() { + if (mStack.size() == 0) + return; + mStack.remove(mStack.size() - 1); + } + + private String mName; + private int mRank; + private long mElapsed; + private long mEndTime; + private ArrayList mStack; + } + + private static class Segment { + Segment(RowData rowData, Block block, long startTime, long endTime) { + mRowData = rowData; + if (block.isContextSwitch()) { + mBlock = block.getParentBlock(); + mIsContextSwitch = true; + } else { + mBlock = block; + } + mStartTime = startTime; + mEndTime = endTime; + } + + private RowData mRowData; + private Block mBlock; + private long mStartTime; + private long mEndTime; + private boolean mIsContextSwitch; + } + + private static class Strip { + Strip(int x, int y, int width, int height, RowData rowData, + Segment segment, Color color) { + mX = x; + mY = y; + mWidth = width; + mHeight = height; + mRowData = rowData; + mSegment = segment; + mColor = color; + } + + int mX; + int mY; + int mWidth; + int mHeight; + RowData mRowData; + Segment mSegment; + Color mColor; + } + + private static class Pixel { + public void setFields(int start, double weight, Segment segment, + Color color, RowData rowData) { + mStart = start; + mMaxWeight = weight; + mSegment = segment; + mColor = color; + mRowData = rowData; + } + + int mStart = -2; // some value that won't match another pixel + double mMaxWeight; + Segment mSegment; + Color mColor; // we need the color here because it may be faded + RowData mRowData; + } + + private static class Range { + Range(int xStart, int width, int y, Color color) { + mXdim.x = xStart; + mXdim.y = width; + mY = y; + mColor = color; + } + + Point mXdim = new Point(0, 0); + int mY; + Color mColor; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java new file mode 100644 index 00000000..35270baf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +final class TraceAction { + public static final int ACTION_ENTER = 0; + public static final int ACTION_EXIT = 1; + public static final int ACTION_INCOMPLETE = 2; + + public final int mAction; + public final Call mCall; + + public TraceAction(int action, Call call) { + mAction = action; + mCall = call; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java new file mode 100644 index 00000000..759e0d69 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.util.ArrayList; +import java.util.HashMap; + +public abstract class TraceReader { + + private TraceUnits mTraceUnits; + + public TraceUnits getTraceUnits() { + if (mTraceUnits == null) + mTraceUnits = new TraceUnits(); + return mTraceUnits; + } + + public ArrayList getThreadTimeRecords() { + return null; + } + + public HashMap getThreadLabels() { + return null; + } + + public MethodData[] getMethods() { + return null; + } + + public ThreadData[] getThreads() { + return null; + } + + public long getTotalCpuTime() { + return 0; + } + + public long getTotalRealTime() { + return 0; + } + + public boolean haveCpuTime() { + return false; + } + + public boolean haveRealTime() { + return false; + } + + public HashMap getProperties() { + return null; + } + + public ProfileProvider getProfileProvider() { + return null; + } + + public TimeBase getPreferredTimeBase() { + return TimeBase.CPU_TIME; + } + + public String getClockSource() { + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java new file mode 100644 index 00000000..60c5fca3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.traceview; + +import java.text.DecimalFormat; + +// This should be a singleton. +public class TraceUnits { + + private TimeScale mTimeScale = TimeScale.MicroSeconds; + private double mScale = 1.0; + DecimalFormat mFormatter = new DecimalFormat(); + + public double getScaledValue(long value) { + return value * mScale; + } + + public double getScaledValue(double value) { + return value * mScale; + } + + public String valueOf(long value) { + return valueOf((double) value); + } + + public String valueOf(double value) { + String pattern; + double scaled = value * mScale; + if ((int) scaled == scaled) + pattern = "###,###"; + else + pattern = "###,###.###"; + mFormatter.applyPattern(pattern); + return mFormatter.format(scaled); + } + + public String labelledString(double value) { + String units = label(); + String num = valueOf(value); + return String.format("%s: %s", units, num); + } + + public String labelledString(long value) { + return labelledString((double) value); + } + + public String label() { + if (mScale == 1.0) + return "usec"; + if (mScale == 0.001) + return "msec"; + if (mScale == 0.000001) + return "sec"; + return null; + } + + public void setTimeScale(TimeScale val) { + mTimeScale = val; + switch (val) { + case Seconds: + mScale = 0.000001; + break; + case MilliSeconds: + mScale = 0.001; + break; + case MicroSeconds: + mScale = 1.0; + break; + } + } + + public TimeScale getTimeScale() { + return mTimeScale; + } + + public enum TimeScale { + Seconds, MilliSeconds, MicroSeconds + }; +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png new file mode 100644 index 00000000..2d4ccc1a Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png differ diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_up.png b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_up.png new file mode 100644 index 00000000..3a0bc3cb Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_up.png differ diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png new file mode 100644 index 00000000..5b4eff1b Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png differ diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml b/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml new file mode 100644 index 00000000..f58e099e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project new file mode 100644 index 00000000..b0c6d667 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.uiautomatorviewer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF new file mode 100644 index 00000000..df859f9c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF @@ -0,0 +1,17 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.uiautomatorviewer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt +Export-Package: com.android.uiautomator, + com.android.uiautomator.actions, + com.android.uiautomator.tree, + images +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/MODULE_LICENSE_APACHE2 b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/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/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle new file mode 100644 index 00000000..8b8ec3c7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle @@ -0,0 +1,26 @@ +group = 'com.android.tools' +archivesBaseName = 'uiautomatorviewer' + +dependencies { + compile project(':base:ddmlib') +} + +sourceSets { + main.resources.srcDir 'src/main/java' +} + +sdk { + linux { + item('etc/uiautomatorviewer') { executable true } + } + mac { + item('etc/uiautomatorviewer') { executable true } + } + windows { + item 'etc/uiautomatorviewer.bat' + } +} + +// configure the manifest of the buildDistributionJar task. +sdkJar.manifest.attributes("Main-Class": "com.android.uiautomator.UiAutomatorViewer") + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer new file mode 100644 index 00000000..868efa7a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Copyright 2012, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Set up prog to be the path of this script, including following symlinks, +# and set up progdir to be the fully-qualified pathname of its directory. +prog="$0" +while [ -h "${prog}" ]; do + newProg=`/bin/ls -ld "${prog}"` + newProg=`expr "${newProg}" : ".* -> \(.*\)$"` + if expr "x${newProg}" : 'x/' >/dev/null; then + prog="${newProg}" + else + progdir=`dirname "${prog}"` + prog="${progdir}/${newProg}" + fi +done +oldwd=`pwd` +progdir=`dirname "${prog}"` +progname=`basename "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/"${progname}" +cd "${oldwd}" + +jarfile=uiautomatorviewer.jar +frameworkdir="$progdir" +libdir="$progdir" +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/tools/lib + libdir=`dirname "$progdir"`/tools/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + frameworkdir=`dirname "$progdir"`/framework + libdir=`dirname "$progdir"`/lib +fi +if [ ! -r "$frameworkdir/$jarfile" ] +then + echo "${progname}: can't find $jarfile" + exit 1 +fi + +javaCmd="java" + +os=`uname` +if [ $os == 'Darwin' ]; then + javaOpts="-Xmx1600M -XstartOnFirstThread" +else + javaOpts="-Xmx1600M" +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +while expr "x$1" : 'x-J' >/dev/null; do + opt=`expr "x$1" : 'x-J\(.*\)'` + javaOpts="${javaOpts} -${opt}" + shift +done + +jarpath="$frameworkdir/$jarfile" + +# Figure out the path to the swt.jar for the current architecture. +# if ANDROID_SWT is defined, then just use this. +# else, if running in the Android source tree, then look for the correct swt folder in prebuilt +# else, look for the correct swt folder in the SDK under tools/lib/ +swtpath="" +if [ -n "$ANDROID_SWT" ]; then + swtpath="$ANDROID_SWT" +else + vmarch=`${javaCmd} -jar "${frameworkdir}"/archquery.jar` + if [ -n "$ANDROID_BUILD_TOP" ]; then + osname=`uname -s | tr A-Z a-z` + swtpath="${ANDROID_BUILD_TOP}/prebuilts/tools/${osname}-${vmarch}/swt" + else + swtpath="${frameworkdir}/${vmarch}" + fi +fi + +# Combine the swtpath and the framework dir path. +if [ -d "$swtpath" ]; then + frameworkdir="${swtpath}:${frameworkdir}" +else + echo "SWT folder '${swtpath}' does not exist." + echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." + exit 1 +fi + +exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.uiautomator.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat new file mode 100644 index 00000000..4739b018 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat @@ -0,0 +1,66 @@ +@echo off +rem Copyright (C) 2012 The Android Open Source Project +rem +rem Licensed under the Apache License, Version 2.0 (the "License"); +rem you may not use this file except in compliance with the License. +rem You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. + +rem don't modify the caller's environment +setlocal + +rem Set up prog to be the path of this script, including following symlinks, +rem and set up progdir to be the fully-qualified pathname of its directory. +set prog=%~f0 + +rem Change current directory and drive to where the script is, to avoid +rem issues with directories containing whitespaces. +cd /d %~dp0 + +rem Get the CWD as a full path with short names only (without spaces) +for %%i in ("%cd%") do set prog_dir=%%~fsi + +rem Check we have a valid Java.exe in the path. +set java_exe= +call lib\find_java.bat +if not defined java_exe goto :EOF + +set jarfile=uiautomatorviewer.jar +set frameworkdir=. + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=lib + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=..\framework + +:JarFileOk + +set jarpath=%frameworkdir%\%jarfile% + +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f "delims=" %%a in ('"%java_exe%" -jar %frameworkdir%\archquery.jar') do set swt_path=%frameworkdir%\%%a + +:SwtDone + +if exist "%swt_path%" goto SetPath + echo SWT folder '%swt_path%' does not exist. + echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. + exit /B + +:SetPath +set javaextdirs=%swt_path%;%frameworkdir% + +call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" "-Dcom.android.uiautomator.bindir=%prog_dir%" -jar %jarpath% %* diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties new file mode 100644 index 00000000..5cdd8cbd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.uiautomatorviewer +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Automator Viewer +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml new file mode 100644 index 00000000..5f8fb29e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.uiautomatorviewer + eclipse-plugin + uiautomatorviewer + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java new file mode 100644 index 00000000..4500f99b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import com.android.SdkConstants; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +public class DebugBridge { + private static AndroidDebugBridge sDebugBridge; + + private static String getAdbLocation() { + String toolsDir = System.getProperty("com.android.uiautomator.bindir"); //$NON-NLS-1$ + if (toolsDir == null) { + return null; + } + + File sdk = new File(toolsDir).getParentFile(); + + // check if adb is present in platform-tools + File platformTools = new File(sdk, "platform-tools"); + File adb = new File(platformTools, SdkConstants.FN_ADB); + if (adb.exists()) { + return adb.getAbsolutePath(); + } + + // check if adb is present in the tools directory + adb = new File(toolsDir, SdkConstants.FN_ADB); + if (adb.exists()) { + return adb.getAbsolutePath(); + } + + // check if we're in the Android source tree where adb is in $ANDROID_HOST_OUT/bin/adb + String androidOut = System.getenv("ANDROID_HOST_OUT"); + if (androidOut != null) { + String adbLocation = androidOut + File.separator + "bin" + File.separator + + SdkConstants.FN_ADB; + if (new File(adbLocation).exists()) { + return adbLocation; + } + } + + return null; + } + + public static void init() { + String adbLocation = getAdbLocation(); + if (adbLocation != null) { + AndroidDebugBridge.init(false /* debugger support */); + sDebugBridge = AndroidDebugBridge.createBridge(adbLocation, false); + } + } + + public static void terminate() { + if (sDebugBridge != null) { + sDebugBridge = null; + AndroidDebugBridge.terminate(); + } + } + + public static boolean isInitialized() { + return sDebugBridge != null; + } + + public static List getDevices() { + return Arrays.asList(sDebugBridge.getDevices()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java new file mode 100644 index 00000000..77657aae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * Implements a file selection dialog for both screen shot and xml dump file + * + * "OK" button won't be enabled unless both files are selected + * It also has a convenience feature such that if one file has been picked, and the other + * file path is empty, then selection for the other file will start from the same base folder + * + */ +public class OpenDialog extends Dialog { + private static final int FIXED_TEXT_FIELD_WIDTH = 300; + private static final int DEFAULT_LAYOUT_SPACING = 10; + private Text mScreenshotText; + private Text mXmlText; + private boolean mFileChanged = false; + private Button mOkButton; + + private static File sScreenshotFile; + private static File sXmlDumpFile; + + /** + * Create the dialog. + * @param parentShell + */ + public OpenDialog(Shell parentShell) { + super(parentShell); + setShellStyle(SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create contents of the dialog. + * @param parent + */ + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout gl_container = new GridLayout(1, false); + gl_container.verticalSpacing = DEFAULT_LAYOUT_SPACING; + gl_container.horizontalSpacing = DEFAULT_LAYOUT_SPACING; + gl_container.marginWidth = DEFAULT_LAYOUT_SPACING; + gl_container.marginHeight = DEFAULT_LAYOUT_SPACING; + container.setLayout(gl_container); + + Group openScreenshotGroup = new Group(container, SWT.NONE); + openScreenshotGroup.setLayout(new GridLayout(2, false)); + openScreenshotGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + openScreenshotGroup.setText("Screenshot"); + + mScreenshotText = new Text(openScreenshotGroup, SWT.BORDER | SWT.READ_ONLY); + if (sScreenshotFile != null) { + mScreenshotText.setText(sScreenshotFile.getAbsolutePath()); + } + GridData gd_screenShotText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gd_screenShotText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; + gd_screenShotText.widthHint = FIXED_TEXT_FIELD_WIDTH; + mScreenshotText.setLayoutData(gd_screenShotText); + + Button openScreenshotButton = new Button(openScreenshotGroup, SWT.NONE); + openScreenshotButton.setText("..."); + openScreenshotButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + handleOpenScreenshotFile(); + } + }); + + Group openXmlGroup = new Group(container, SWT.NONE); + openXmlGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + openXmlGroup.setText("UI XML Dump"); + openXmlGroup.setLayout(new GridLayout(2, false)); + + mXmlText = new Text(openXmlGroup, SWT.BORDER | SWT.READ_ONLY); + mXmlText.setEditable(false); + if (sXmlDumpFile != null) { + mXmlText.setText(sXmlDumpFile.getAbsolutePath()); + } + GridData gd_xmlText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gd_xmlText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; + gd_xmlText.widthHint = FIXED_TEXT_FIELD_WIDTH; + mXmlText.setLayoutData(gd_xmlText); + + Button openXmlButton = new Button(openXmlGroup, SWT.NONE); + openXmlButton.setText("..."); + openXmlButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + handleOpenXmlDumpFile(); + } + }); + + return container; + } + + /** + * Create contents of the button bar. + * @param parent + */ + @Override + protected void createButtonsForButtonBar(Composite parent) { + mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + updateButtonState(); + } + + /** + * Return the initial size of the dialog. + */ + @Override + protected Point getInitialSize() { + return new Point(368, 233); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Open UI Dump Files"); + } + + private void handleOpenScreenshotFile() { + FileDialog fd = new FileDialog(getShell(), SWT.OPEN); + fd.setText("Open Screenshot File"); + File initialFile = sScreenshotFile; + // if file has never been selected before, try to base initial path on the mXmlDumpFile + if (initialFile == null && sXmlDumpFile != null && sXmlDumpFile.isFile()) { + initialFile = sXmlDumpFile.getParentFile(); + } + if (initialFile != null) { + if (initialFile.isFile()) { + fd.setFileName(initialFile.getAbsolutePath()); + } else if (initialFile.isDirectory()) { + fd.setFilterPath(initialFile.getAbsolutePath()); + } + } + String[] filter = {"*.png"}; + fd.setFilterExtensions(filter); + String selected = fd.open(); + if (selected != null) { + sScreenshotFile = new File(selected); + mScreenshotText.setText(selected); + mFileChanged = true; + } + updateButtonState(); + } + + private void handleOpenXmlDumpFile() { + FileDialog fd = new FileDialog(getShell(), SWT.OPEN); + fd.setText("Open UI Dump XML File"); + File initialFile = sXmlDumpFile; + // if file has never been selected before, try to base initial path on the mScreenshotFile + if (initialFile == null && sScreenshotFile != null && sScreenshotFile.isFile()) { + initialFile = sScreenshotFile.getParentFile(); + } + if (initialFile != null) { + if (initialFile.isFile()) { + fd.setFileName(initialFile.getAbsolutePath()); + } else if (initialFile.isDirectory()) { + fd.setFilterPath(initialFile.getAbsolutePath()); + } + } + String initialPath = mXmlText.getText(); + if (initialPath.isEmpty() && sScreenshotFile != null && sScreenshotFile.isFile()) { + initialPath = sScreenshotFile.getParentFile().getAbsolutePath(); + } + String[] filter = {"*.uix"}; + fd.setFilterExtensions(filter); + String selected = fd.open(); + if (selected != null) { + sXmlDumpFile = new File(selected); + mXmlText.setText(selected); + mFileChanged = true; + } + updateButtonState(); + } + + private void updateButtonState() { + mOkButton.setEnabled(sXmlDumpFile != null && sXmlDumpFile.isFile()); + } + + public boolean hasFileChanged() { + return mFileChanged; + } + + public File getScreenshotFile() { + return sScreenshotFile; + } + + public File getXmlDumpFile() { + return sXmlDumpFile; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java new file mode 100644 index 00000000..c4daa1c3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.SyncService; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.RootWindowNode; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.widgets.Display; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class UiAutomatorHelper { + public static final int UIAUTOMATOR_MIN_API_LEVEL = 16; + + private static final String UIAUTOMATOR = "/system/bin/uiautomator"; //$NON-NLS-1$ + private static final String UIAUTOMATOR_DUMP_COMMAND = "dump"; //$NON-NLS-1$ + private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml"; //$NON-NLS-1$ + private static final int XML_CAPTURE_TIMEOUT_SEC = 40; + + private static boolean supportsUiAutomator(IDevice device) { + String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); + int apiLevel; + try { + apiLevel = Integer.parseInt(apiLevelString); + } catch (NumberFormatException e) { + apiLevel = UIAUTOMATOR_MIN_API_LEVEL; + } + + return apiLevel >= UIAUTOMATOR_MIN_API_LEVEL; + } + + private static void getUiHierarchyFile(IDevice device, File dst, + IProgressMonitor monitor, boolean compressed) { + if (monitor == null) { + monitor = new NullProgressMonitor(); + } + + monitor.subTask("Deleting old UI XML snapshot ..."); + String command = "rm " + UIDUMP_DEVICE_PATH; + + try { + CountDownLatch commandCompleteLatch = new CountDownLatch(1); + device.executeShellCommand(command, + new CollectingOutputReceiver(commandCompleteLatch)); + commandCompleteLatch.await(5, TimeUnit.SECONDS); + } catch (Exception e1) { + // ignore exceptions while deleting stale files + } + + monitor.subTask("Taking UI XML snapshot..."); + if (compressed){ + command = String.format("%s %s --compressed %s", UIAUTOMATOR, + UIAUTOMATOR_DUMP_COMMAND, + UIDUMP_DEVICE_PATH); + } else { + command = String.format("%s %s %s", UIAUTOMATOR, + UIAUTOMATOR_DUMP_COMMAND, + UIDUMP_DEVICE_PATH); + } + CountDownLatch commandCompleteLatch = new CountDownLatch(1); + + try { + device.executeShellCommand( + command, + new CollectingOutputReceiver(commandCompleteLatch), + XML_CAPTURE_TIMEOUT_SEC * 1000); + commandCompleteLatch.await(XML_CAPTURE_TIMEOUT_SEC, TimeUnit.SECONDS); + + monitor.subTask("Pull UI XML snapshot from device..."); + device.getSyncService().pullFile(UIDUMP_DEVICE_PATH, + dst.getAbsolutePath(), SyncService.getNullProgressMonitor()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + //to maintain a backward compatible api, use non-compressed as default snapshot type + public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor) + throws UiAutomatorException { + return takeSnapshot(device, monitor,false); + } + + public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor, + boolean compressed) throws UiAutomatorException { + if (monitor == null) { + monitor = new NullProgressMonitor(); + } + + monitor.subTask("Checking if device support UI Automator"); + if (!supportsUiAutomator(device)) { + String msg = "UI Automator requires a device with API Level " + + UIAUTOMATOR_MIN_API_LEVEL; + throw new UiAutomatorException(msg, null); + } + + monitor.subTask("Creating temporary files for uiautomator results."); + File tmpDir = null; + File xmlDumpFile = null; + File screenshotFile = null; + try { + tmpDir = File.createTempFile("uiautomatorviewer_", ""); + tmpDir.delete(); + if (!tmpDir.mkdirs()) + throw new IOException("Failed to mkdir"); + xmlDumpFile = File.createTempFile("dump_", ".uix", tmpDir); + screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir); + } catch (Exception e) { + String msg = "Error while creating temporary file to save snapshot: " + + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + tmpDir.deleteOnExit(); + xmlDumpFile.deleteOnExit(); + screenshotFile.deleteOnExit(); + + monitor.subTask("Obtaining UI hierarchy"); + try { + UiAutomatorHelper.getUiHierarchyFile(device, xmlDumpFile, monitor, compressed); + } catch (Exception e) { + String msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + UiAutomatorModel model; + try { + model = new UiAutomatorModel(xmlDumpFile); + } catch (Exception e) { + String msg = "Error while parsing UI hierarchy XML file: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + monitor.subTask("Obtaining device screenshot"); + RawImage rawImage; + try { + rawImage = device.getScreenshot(); + } catch (Exception e) { + String msg = "Error taking device screenshot: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + // rotate the screen shot per device rotation + BasicTreeNode root = model.getXmlRootNode(); + if (root instanceof RootWindowNode) { + for (int i = 0; i < ((RootWindowNode)root).getRotation(); i++) { + rawImage = rawImage.getRotated(); + } + } + PaletteData palette = new PaletteData( + rawImage.getRedMask(), + rawImage.getGreenMask(), + rawImage.getBlueMask()); + ImageData imageData = new ImageData(rawImage.width, rawImage.height, + rawImage.bpp, palette, 1, rawImage.data); + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { imageData }; + loader.save(screenshotFile.getAbsolutePath(), SWT.IMAGE_PNG); + Image screenshot = new Image(Display.getDefault(), imageData); + + return new UiAutomatorResult(xmlDumpFile, model, screenshot); + } + + @SuppressWarnings("serial") + public static class UiAutomatorException extends Exception { + public UiAutomatorException(String msg, Throwable t) { + super(msg, t); + } + } + + public static class UiAutomatorResult { + public final File uiHierarchy; + public final UiAutomatorModel model; + public final Image screenshot; + + public UiAutomatorResult(File uiXml, UiAutomatorModel m, Image s) { + uiHierarchy = uiXml; + model = m; + screenshot = s; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java new file mode 100644 index 00000000..c3ba9e68 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import com.android.uiautomator.tree.AttributePair; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener; +import com.android.uiautomator.tree.UiHierarchyXmlLoader; +import com.android.uiautomator.tree.UiNode; + +import org.eclipse.swt.graphics.Rectangle; + +import java.io.File; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class UiAutomatorModel { + private BasicTreeNode mRootNode; + private BasicTreeNode mSelectedNode; + private Rectangle mCurrentDrawingRect; + private List mNafNodes; + + // determines whether we lookup the leaf UI node on mouse move of screenshot image + private boolean mExploreMode = true; + + private boolean mShowNafNodes = false; + private List mNodelist; + private Set mSearchKeySet = new HashSet(); + + public UiAutomatorModel(File xmlDumpFile) { + mSearchKeySet.add("text"); + mSearchKeySet.add("content-desc"); + + UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader(); + BasicTreeNode rootNode = loader.parseXml(xmlDumpFile.getAbsolutePath()); + if (rootNode == null) { + System.err.println("null rootnode after parsing."); + throw new IllegalArgumentException("Invalid ui automator hierarchy file."); + } + + mNafNodes = loader.getNafNodes(); + if (mRootNode != null) { + mRootNode.clearAllChildren(); + } + + mRootNode = rootNode; + mExploreMode = true; + mNodelist = loader.getAllNodes(); + } + + public BasicTreeNode getXmlRootNode() { + return mRootNode; + } + + public BasicTreeNode getSelectedNode() { + return mSelectedNode; + } + + /** + * change node selection in the Model recalculate the rect to highlight, + * also notifies the View to refresh accordingly + * + * @param node + */ + public void setSelectedNode(BasicTreeNode node) { + mSelectedNode = node; + if (mSelectedNode instanceof UiNode) { + UiNode uiNode = (UiNode) mSelectedNode; + mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height); + } else { + mCurrentDrawingRect = null; + } + } + + public Rectangle getCurrentDrawingRect() { + return mCurrentDrawingRect; + } + + /** + * Do a search in tree to find a leaf node or deepest parent node containing the coordinate + * + * @param x + * @param y + * @return + */ + public BasicTreeNode updateSelectionForCoordinates(int x, int y) { + BasicTreeNode node = null; + + if (mRootNode != null) { + MinAreaFindNodeListener listener = new MinAreaFindNodeListener(); + boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener); + if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) { + node = listener.mNode; + } + } + + return node; + } + + public boolean isExploreMode() { + return mExploreMode; + } + + public void toggleExploreMode() { + mExploreMode = !mExploreMode; + } + + public void setExploreMode(boolean exploreMode) { + mExploreMode = exploreMode; + } + + private static class MinAreaFindNodeListener implements IFindNodeListener { + BasicTreeNode mNode = null; + + @Override + public void onFoundNode(BasicTreeNode node) { + if (mNode == null) { + mNode = node; + } else { + if ((node.height * node.width) < (mNode.height * mNode.width)) { + mNode = node; + } + } + } + } + + public List getNafNodes() { + return mNafNodes; + } + + public void toggleShowNaf() { + mShowNafNodes = !mShowNafNodes; + } + + public boolean shouldShowNafNodes() { + return mShowNafNodes; + } + + public List searchNode(String tofind) { + List result = new LinkedList(); + for (BasicTreeNode node : mNodelist) { + Object[] attrs = node.getAttributesArray(); + for (Object attr : attrs) { + if (!mSearchKeySet.contains(((AttributePair) attr).key)) + continue; + if (((AttributePair) attr).value.toLowerCase().contains(tofind.toLowerCase())) { + result.add(node); + break; + } + } + } + return result; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java new file mode 100644 index 00000000..c6e95113 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import com.android.uiautomator.actions.ExpandAllAction; +import com.android.uiautomator.actions.ImageHelper; +import com.android.uiautomator.actions.ToggleNafAction; +import com.android.uiautomator.tree.AttributePair; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.BasicTreeNodeContentProvider; +import com.android.uiautomator.tree.UiNode; + +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.layout.TableColumnLayout; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; + +import java.io.File; +import java.util.List; + +public class UiAutomatorView extends Composite { + private static final int IMG_BORDER = 2; + + // The screenshot area is made of a stack layout of two components: screenshot canvas and + // a "specify screenshot" button. If a screenshot is already available, then that is displayed + // on the canvas. If it is not availble, then the "specify screenshot" button is displayed. + private Composite mScreenshotComposite; + private StackLayout mStackLayout; + private Composite mSetScreenshotComposite; + private Canvas mScreenshotCanvas; + + private TreeViewer mTreeViewer; + private TableViewer mTableViewer; + + private float mScale = 1.0f; + private int mDx, mDy; + + private UiAutomatorModel mModel; + private File mModelFile; + private Image mScreenshot; + + private List mSearchResult; + private int mSearchResultIndex; + private ToolItem itemDeleteAndInfo; + private Text searchTextarea; + private Cursor mOrginialCursor; + private ToolItem itemPrev, itemNext; + private ToolItem coordinateLabel; + + private String mLastSearchedTerm; + + private Cursor mCrossCursor; + + public UiAutomatorView(Composite parent, int style) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + + SashForm baseSash = new SashForm(this, SWT.HORIZONTAL); + mOrginialCursor = getShell().getCursor(); + mCrossCursor = new Cursor(getDisplay(), SWT.CURSOR_CROSS); + mScreenshotComposite = new Composite(baseSash, SWT.BORDER); + mStackLayout = new StackLayout(); + mScreenshotComposite.setLayout(mStackLayout); + // draw the canvas with border, so the divider area for sash form can be highlighted + mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER); + mStackLayout.topControl = mScreenshotCanvas; + mScreenshotComposite.layout(); + + // set cursor when enter canvas + mScreenshotCanvas.addListener(SWT.MouseEnter, new Listener() { + @Override + public void handleEvent(Event arg0) { + getShell().setCursor(mCrossCursor); + } + }); + mScreenshotCanvas.addListener(SWT.MouseExit, new Listener() { + @Override + public void handleEvent(Event arg0) { + getShell().setCursor(mOrginialCursor); + } + }); + + mScreenshotCanvas.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + if (mModel != null) { + mModel.toggleExploreMode(); + redrawScreenshot(); + } + } + }); + mScreenshotCanvas.setBackground( + getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); + mScreenshotCanvas.addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + if (mScreenshot != null) { + updateScreenshotTransformation(); + // shifting the image here, so that there's a border around screen shot + // this makes highlighting red rectangles on the screen shot edges more visible + Transform t = new Transform(e.gc.getDevice()); + t.translate(mDx, mDy); + t.scale(mScale, mScale); + e.gc.setTransform(t); + e.gc.drawImage(mScreenshot, 0, 0); + // this resets the transformation to identity transform, i.e. no change + // we don't use transformation here because it will cause the line pattern + // and line width of highlight rect to be scaled, causing to appear to be blurry + e.gc.setTransform(null); + if (mModel.shouldShowNafNodes()) { + // highlight the "Not Accessibility Friendly" nodes + e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + for (Rectangle r : mModel.getNafNodes()) { + e.gc.setAlpha(50); + e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), + getScaledSize(r.width), getScaledSize(r.height)); + e.gc.setAlpha(255); + e.gc.setLineStyle(SWT.LINE_SOLID); + e.gc.setLineWidth(2); + e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), + getScaledSize(r.width), getScaledSize(r.height)); + } + } + + // draw the search result rects + if (mSearchResult != null){ + for (BasicTreeNode result : mSearchResult){ + if (result instanceof UiNode) { + UiNode uiNode = (UiNode) result; + Rectangle rect = new Rectangle( + uiNode.x, uiNode.y, uiNode.width, uiNode.height); + e.gc.setForeground( + e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + e.gc.setLineStyle(SWT.LINE_DASH); + e.gc.setLineWidth(1); + e.gc.drawRectangle(mDx + getScaledSize(rect.x), + mDy + getScaledSize(rect.y), + getScaledSize(rect.width), getScaledSize(rect.height)); + } + } + } + + // draw the mouseover rects + Rectangle rect = mModel.getCurrentDrawingRect(); + if (rect != null) { + e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED)); + if (mModel.isExploreMode()) { + // when we highlight nodes dynamically on mouse move, + // use dashed borders + e.gc.setLineStyle(SWT.LINE_DASH); + e.gc.setLineWidth(1); + } else { + // when highlighting nodes on tree node selection, + // use solid borders + e.gc.setLineStyle(SWT.LINE_SOLID); + e.gc.setLineWidth(2); + } + e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), + getScaledSize(rect.width), getScaledSize(rect.height)); + } + } + } + }); + mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + if (mModel != null) { + int x = getInverseScaledSize(e.x - mDx); + int y = getInverseScaledSize(e.y - mDy); + // show coordinate + coordinateLabel.setText(String.format("(%d,%d)", x,y)); + if (mModel.isExploreMode()) { + BasicTreeNode node = mModel.updateSelectionForCoordinates(x, y); + if (node != null) { + updateTreeSelection(node); + } + } + } + } + }); + + mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE); + mSetScreenshotComposite.setLayout(new GridLayout()); + + final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH); + setScreenshotButton.setText("Specify Screenshot..."); + setScreenshotButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + FileDialog fd = new FileDialog(setScreenshotButton.getShell()); + fd.setFilterExtensions(new String[] {"*.png" }); + if (mModelFile != null) { + fd.setFilterPath(mModelFile.getParent()); + } + String screenshotPath = fd.open(); + if (screenshotPath == null) { + return; + } + + ImageData[] data; + try { + data = new ImageLoader().load(screenshotPath); + } catch (Exception e) { + return; + } + + // "data" is an array, probably used to handle images that has multiple frames + // i.e. gifs or icons, we just care if it has at least one here + if (data.length < 1) { + return; + } + + mScreenshot = new Image(Display.getDefault(), data[0]); + redrawScreenshot(); + } + }); + + // right sash is split into 2 parts: upper-right and lower-right + // both are composites with borders, so that the horizontal divider can be highlighted by + // the borders + SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL); + + // upper-right base contains the toolbar and the tree + Composite upperRightBase = new Composite(rightSash, SWT.BORDER); + upperRightBase.setLayout(new GridLayout(1, false)); + + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + toolBarManager.add(new ExpandAllAction(this)); + toolBarManager.add(new ToggleNafAction(this)); + ToolBar searchtoolbar = toolBarManager.createControl(upperRightBase); + + // add search box and navigation buttons for search results + ToolItem itemSeparator = new ToolItem(searchtoolbar, SWT.SEPARATOR | SWT.RIGHT); + searchTextarea = new Text(searchtoolbar, SWT.BORDER | SWT.SINGLE | SWT.SEARCH); + searchTextarea.pack(); + itemSeparator.setWidth(searchTextarea.getBounds().width); + itemSeparator.setControl(searchTextarea); + itemPrev = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemPrev.setImage(ImageHelper.loadImageDescriptorFromResource("images/prev.png") + .createImage()); + itemNext = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemNext.setImage(ImageHelper.loadImageDescriptorFromResource("images/next.png") + .createImage()); + itemDeleteAndInfo = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemDeleteAndInfo.setImage(ImageHelper.loadImageDescriptorFromResource("images/delete.png") + .createImage()); + itemDeleteAndInfo.setToolTipText("Clear search results"); + coordinateLabel = new ToolItem(searchtoolbar, SWT.SIMPLE); + coordinateLabel.setText(""); + coordinateLabel.setEnabled(false); + + // add search function + searchTextarea.addKeyListener(new KeyListener() { + @Override + public void keyReleased(KeyEvent event) { + if (event.keyCode == SWT.CR) { + String term = searchTextarea.getText(); + if (!term.isEmpty()) { + if (term.equals(mLastSearchedTerm)) { + nextSearchResult(); + return; + } + clearSearchResult(); + mSearchResult = mModel.searchNode(term); + if (!mSearchResult.isEmpty()) { + mSearchResultIndex = 0; + updateSearchResultSelection(); + mLastSearchedTerm = term; + } + } + } + } + + @Override + public void keyPressed(KeyEvent event) { + } + }); + SelectionListener l = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent se) { + if (se.getSource() == itemPrev) { + prevSearchResult(); + } else if (se.getSource() == itemNext) { + nextSearchResult(); + } else if (se.getSource() == itemDeleteAndInfo) { + searchTextarea.setText(""); + clearSearchResult(); + } + } + }; + itemPrev.addSelectionListener(l); + itemNext.addSelectionListener(l); + itemDeleteAndInfo.addSelectionListener(l); + + searchtoolbar.pack(); + searchtoolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE); + mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider()); + // default LabelProvider uses toString() to generate text to display + mTreeViewer.setLabelProvider(new LabelProvider()); + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + BasicTreeNode selectedNode = null; + if (event.getSelection() instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + Object o = selection.getFirstElement(); + if (o instanceof BasicTreeNode) { + selectedNode = (BasicTreeNode) o; + } + } + + mModel.setSelectedNode(selectedNode); + redrawScreenshot(); + if (selectedNode != null) { + loadAttributeTable(); + } + } + }); + Tree tree = mTreeViewer.getTree(); + tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + // move focus so that it's not on tool bar (looks weird) + tree.setFocus(); + + // lower-right base contains the detail group + Composite lowerRightBase = new Composite(rightSash, SWT.BORDER); + lowerRightBase.setLayout(new FillLayout()); + Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE); + grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL)); + grpNodeDetail.setText("Node Detail"); + + Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE); + + TableColumnLayout columnLayout = new TableColumnLayout(); + tableContainer.setLayout(columnLayout); + + mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION); + Table table = mTableViewer.getTable(); + table.setLinesVisible(true); + // use ArrayContentProvider here, it assumes the input to the TableViewer + // is an array, where each element represents a row in the table + mTableViewer.setContentProvider(new ArrayContentProvider()); + + TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE); + TableColumn tblclmnKey = tableViewerColumnKey.getColumn(); + tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof AttributePair) { + // first column, shows the attribute name + return ((AttributePair) element).key; + } + return super.getText(element); + } + }); + columnLayout.setColumnData(tblclmnKey, + new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); + + TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE); + tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer)); + TableColumn tblclmnValue = tableViewerColumnValue.getColumn(); + columnLayout.setColumnData(tblclmnValue, + new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true)); + tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof AttributePair) { + // second column, shows the attribute value + return ((AttributePair) element).value; + } + return super.getText(element); + } + }); + // sets the ratio of the vertical split: left 5 vs right 3 + baseSash.setWeights(new int[] {5, 3 }); + } + + protected void prevSearchResult() { + if (mSearchResult == null) + return; + if(mSearchResult.isEmpty()){ + mSearchResult = null; + return; + } + mSearchResultIndex = mSearchResultIndex - 1; + if (mSearchResultIndex < 0){ + mSearchResultIndex += mSearchResult.size(); + } + updateSearchResultSelection(); + } + protected void clearSearchResult() { + itemDeleteAndInfo.setText(""); + mSearchResult = null; + mSearchResultIndex = 0; + mLastSearchedTerm = ""; + mScreenshotCanvas.redraw(); + } + protected void nextSearchResult() { + if (mSearchResult == null) + return; + if(mSearchResult.isEmpty()){ + mSearchResult = null; + return; + } + mSearchResultIndex = (mSearchResultIndex + 1) % mSearchResult.size(); + updateSearchResultSelection(); + } + + private void updateSearchResultSelection() { + updateTreeSelection(mSearchResult.get(mSearchResultIndex)); + itemDeleteAndInfo.setText("" + (mSearchResultIndex + 1) + "/" + + mSearchResult.size()); + } + + private int getScaledSize(int size) { + if (mScale == 1.0f) { + return size; + } else { + return new Double(Math.floor((size * mScale))).intValue(); + } + } + + private int getInverseScaledSize(int size) { + if (mScale == 1.0f) { + return size; + } else { + return new Double(Math.floor((size / mScale))).intValue(); + } + } + + private void updateScreenshotTransformation() { + Rectangle canvas = mScreenshotCanvas.getBounds(); + Rectangle image = mScreenshot.getBounds(); + float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float) image.width; + float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float) image.height; + + // use the smaller scale here so that we can fit the entire screenshot + mScale = Math.min(scaleX, scaleY); + // calculate translation values to center the image on the canvas + mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER; + mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER; + } + + private class AttributeTableEditingSupport extends EditingSupport { + + private TableViewer mViewer; + + public AttributeTableEditingSupport(TableViewer viewer) { + super(viewer); + mViewer = viewer; + } + + @Override + protected boolean canEdit(Object arg0) { + return true; + } + + @Override + protected CellEditor getCellEditor(Object arg0) { + return new TextCellEditor(mViewer.getTable()); + } + + @Override + protected Object getValue(Object o) { + return ((AttributePair) o).value; + } + + @Override + protected void setValue(Object arg0, Object arg1) { + } + } + + /** + * Causes a redraw of the canvas. + * + * The drawing code of canvas will handle highlighted nodes and etc based on data + * retrieved from Model + */ + public void redrawScreenshot() { + if (mScreenshot == null) { + mStackLayout.topControl = mSetScreenshotComposite; + } else { + mStackLayout.topControl = mScreenshotCanvas; + } + mScreenshotComposite.layout(); + + mScreenshotCanvas.redraw(); + } + + public void setInputHierarchy(Object input) { + mTreeViewer.setInput(input); + } + + public void loadAttributeTable() { + // update the lower right corner table to show the attributes of the node + mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray()); + } + + public void expandAll() { + mTreeViewer.expandAll(); + } + + public void updateTreeSelection(BasicTreeNode node) { + mTreeViewer.setSelection(new StructuredSelection(node), true); + } + + public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) { + mModel = model; + mModelFile = modelBackingFile; + + if (mScreenshot != null) { + mScreenshot.dispose(); + } + mScreenshot = screenshot; + clearSearchResult(); + redrawScreenshot(); + // load xml into tree + BasicTreeNode wrapper = new BasicTreeNode(); + // putting another root node on top of existing root node + // because Tree seems to like to hide the root node + wrapper.addChild(mModel.getXmlRootNode()); + setInputHierarchy(wrapper); + mTreeViewer.getTree().setFocus(); + + } + + public boolean shouldShowNafNodes() { + return mModel != null ? mModel.shouldShowNafNodes() : false; + } + + public void toggleShowNaf() { + if (mModel != null) { + mModel.toggleShowNaf(); + } + } + + public Image getScreenShot() { + return mScreenshot; + } + + public File getModelFile() { + return mModelFile; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java new file mode 100644 index 00000000..1a680012 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator; + +import com.android.uiautomator.actions.OpenFilesAction; +import com.android.uiautomator.actions.SaveScreenShotAction; +import com.android.uiautomator.actions.ScreenshotAction; + +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.window.ApplicationWindow; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.ToolBar; + +import java.io.File; + +public class UiAutomatorViewer extends ApplicationWindow { + private UiAutomatorView mUiAutomatorView; + public UiAutomatorViewer() { + super(null); + } + + @Override + protected Control createContents(Composite parent) { + Composite c = new Composite(parent, SWT.BORDER); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + c.setLayout(gridLayout); + + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + c.setLayoutData(gd); + + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + toolBarManager.add(new OpenFilesAction(this)); + toolBarManager.add(new ScreenshotAction(this,false)); + toolBarManager.add(new ScreenshotAction(this,true)); + toolBarManager.add(new SaveScreenShotAction(this)); + ToolBar tb = toolBarManager.createControl(c); + tb.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mUiAutomatorView = new UiAutomatorView(c, SWT.BORDER); + mUiAutomatorView.setLayoutData(new GridData(GridData.FILL_BOTH)); + + return parent; + } + + public static void main(String args[]) { + DebugBridge.init(); + + try { + UiAutomatorViewer window = new UiAutomatorViewer(); + window.setBlockOnOpen(true); + window.open(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + DebugBridge.terminate(); + } + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("UI Automator Viewer"); + } + + @Override + protected Point getInitialSize() { + return new Point(800, 600); + } + + public void setModel(final UiAutomatorModel model, final File modelFile, + final Image screenshot) { + if (Display.getDefault().getThread() != Thread.currentThread()) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mUiAutomatorView.setModel(model, modelFile, screenshot); + } + }); + } else { + mUiAutomatorView.setModel(model, modelFile, screenshot); + } + } + public Image getScreenShot() { + return mUiAutomatorView.getScreenShot(); + } + public File getModelFile(){ + return mUiAutomatorView.getModelFile(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java new file mode 100644 index 00000000..266f69f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorView; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; + +public class ExpandAllAction extends Action { + + UiAutomatorView mView; + + public ExpandAllAction(UiAutomatorView view) { + super("&Expand All"); + mView = view;; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/expandall.png"); + } + + @Override + public void run() { + mView.expandAll(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java new file mode 100644 index 00000000..58cfc228 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; + +import java.io.IOException; +import java.io.InputStream; + +public class ImageHelper { + + public static ImageDescriptor loadImageDescriptorFromResource(String path) { + InputStream is = ImageHelper.class.getClassLoader().getResourceAsStream(path); + if (is != null) { + ImageData[] data = null; + try { + data = new ImageLoader().load(is); + } catch (SWTException e) { + } finally { + try { + is.close(); + } catch (IOException e) { + } + } + if (data != null && data.length > 0) { + return ImageDescriptor.createFromImageData(data[0]); + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java new file mode 100644 index 00000000..2e108852 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import com.android.uiautomator.OpenDialog; +import com.android.uiautomator.UiAutomatorModel; +import com.android.uiautomator.UiAutomatorViewer; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; + +import java.io.File; + +public class OpenFilesAction extends Action { + private UiAutomatorViewer mViewer; + + public OpenFilesAction(UiAutomatorViewer viewer) { + super("&Open"); + + mViewer = viewer; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/open-folder.png"); + } + + @Override + public void run() { + OpenDialog d = new OpenDialog(Display.getDefault().getActiveShell()); + if (d.open() != OpenDialog.OK) { + return; + } + + UiAutomatorModel model; + try { + model = new UiAutomatorModel(d.getXmlDumpFile()); + } catch (Exception e) { + // FIXME: show error + return; + } + + Image img = null; + File screenshot = d.getScreenshotFile(); + if (screenshot != null) { + try { + ImageData[] data = new ImageLoader().load(screenshot.getAbsolutePath()); + + // "data" is an array, probably used to handle images that has multiple frames + // i.e. gifs or icons, we just care if it has at least one here + if (data.length < 1) { + throw new RuntimeException("Unable to load image: " + + screenshot.getAbsolutePath()); + } + + img = new Image(Display.getDefault(), data[0]); + } catch (Exception e) { + // FIXME: show error + return; + } + } + + mViewer.setModel(model, d.getXmlDumpFile(), img); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java new file mode 100644 index 00000000..97d6621e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorViewer; +import com.google.common.io.Files; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; + +import java.io.File; + +public class SaveScreenShotAction extends Action { + private static final String PNG_TYPE = ".png"; + private static final String UIX_TYPE = ".uix"; + private UiAutomatorViewer mViewer; + public SaveScreenShotAction(UiAutomatorViewer viewer) { + super("&Save"); + mViewer = viewer; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/save.png"); + } + + @Override + public void run() { + final Image screenshot = mViewer.getScreenShot(); + final File model = mViewer.getModelFile(); + if (model == null || screenshot == null) { + return; + } + DirectoryDialog dd = new DirectoryDialog(Display.getDefault().getActiveShell()); + dd.setText("Save Screenshot and UiX Files"); + final String path = dd.open(); + if (path == null) { + return; + } + + // to prevent blocking the ui thread, we do the saving in the other thread. + new Thread(){ + String filepath; + @Override + public void run() { + filepath = new File(path, model.getName()).toString(); + filepath = filepath.substring(0,filepath.lastIndexOf(".")); + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + screenshot.getImageData() }; + try { + imageLoader.save(filepath + PNG_TYPE, SWT.IMAGE_PNG); + Files.copy(model, new File(filepath + UIX_TYPE)); + } catch (final Exception e) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Status status = new Status(IStatus.ERROR, + "Error writing file", e.getLocalizedMessage()); + ErrorDialog.openError(Display.getDefault().getActiveShell(), + String.format("Error writing %s.uix", filepath), + e.getLocalizedMessage(), status); + } + }); + } + }; + }.start(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java new file mode 100644 index 00000000..fc021097 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import com.android.ddmlib.IDevice; +import com.android.uiautomator.DebugBridge; +import com.android.uiautomator.UiAutomatorHelper; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; +import com.android.uiautomator.UiAutomatorViewer; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class ScreenshotAction extends Action { + UiAutomatorViewer mViewer; + private boolean mCompressed; + + public ScreenshotAction(UiAutomatorViewer viewer, boolean compressed) { + super("&Device Screenshot "+ (compressed ? "with Compressed Hierarchy" : "") + +"(uiautomator dump" + (compressed ? " --compressed)" : ")")); + mViewer = viewer; + mCompressed = compressed; + } + + @Override + public ImageDescriptor getImageDescriptor() { + if(mCompressed) + return ImageHelper.loadImageDescriptorFromResource("images/screenshotcompressed.png"); + else + return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png"); + } + + @Override + public void run() { + if (!DebugBridge.isInitialized()) { + MessageDialog.openError(mViewer.getShell(), + "Error obtaining Device Screenshot", + "Unable to connect to adb. Check if adb is installed correctly."); + return; + } + + final IDevice device = pickDevice(); + if (device == null) { + return; + } + + ProgressMonitorDialog dialog = new ProgressMonitorDialog(mViewer.getShell()); + try { + dialog.run(true, false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, + InterruptedException { + UiAutomatorResult result = null; + try { + result = UiAutomatorHelper.takeSnapshot(device, monitor, mCompressed); + } catch (UiAutomatorException e) { + monitor.done(); + showError(e.getMessage(), e); + return; + } + + mViewer.setModel(result.model, result.uiHierarchy, result.screenshot); + monitor.done(); + } + }); + } catch (Exception e) { + showError("Unexpected error while obtaining UI hierarchy", e); + } + } + + private void showError(final String msg, final Throwable t) { + mViewer.getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + Status s = new Status(IStatus.ERROR, "Screenshot", msg, t); + ErrorDialog.openError( + mViewer.getShell(), "Error", "Error obtaining UI hierarchy", s); + } + }); + } + + private IDevice pickDevice() { + List devices = DebugBridge.getDevices(); + if (devices.size() == 0) { + MessageDialog.openError(mViewer.getShell(), + "Error obtaining Device Screenshot", + "No Android devices were detected by adb."); + return null; + } else if (devices.size() == 1) { + return devices.get(0); + } else { + DevicePickerDialog dlg = new DevicePickerDialog(mViewer.getShell(), devices); + if (dlg.open() != Window.OK) { + return null; + } + return dlg.getSelectedDevice(); + } + } + + private static class DevicePickerDialog extends Dialog { + private final List mDevices; + private final String[] mDeviceNames; + private static int sSelectedDeviceIndex; + + public DevicePickerDialog(Shell parentShell, List devices) { + super(parentShell); + + mDevices = devices; + mDeviceNames = new String[mDevices.size()]; + for (int i = 0; i < devices.size(); i++) { + mDeviceNames[i] = devices.get(i).getName(); + } + } + + @Override + protected Control createDialogArea(Composite parentShell) { + Composite parent = (Composite) super.createDialogArea(parentShell); + Composite c = new Composite(parent, SWT.NONE); + + c.setLayout(new GridLayout(2, false)); + + Label l = new Label(c, SWT.NONE); + l.setText("Select device: "); + + final Combo combo = new Combo(c, SWT.BORDER | SWT.READ_ONLY); + combo.setItems(mDeviceNames); + int defaultSelection = + sSelectedDeviceIndex < mDevices.size() ? sSelectedDeviceIndex : 0; + combo.select(defaultSelection); + sSelectedDeviceIndex = defaultSelection; + + combo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + sSelectedDeviceIndex = combo.getSelectionIndex(); + } + }); + + return parent; + } + + public IDevice getSelectedDevice() { + return mDevices.get(sSelectedDeviceIndex); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java new file mode 100644 index 00000000..082e219f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorView; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.resource.ImageDescriptor; + +public class ToggleNafAction extends Action { + private UiAutomatorView mView; + + public ToggleNafAction(UiAutomatorView view) { + super("&Toggle NAF Nodes", IAction.AS_CHECK_BOX); + setChecked(view.shouldShowNafNodes()); + + mView = view; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/warning.png"); + } + + @Override + public void run() { + mView.toggleShowNaf(); + mView.redrawScreenshot(); + setChecked(mView.shouldShowNafNodes()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java new file mode 100644 index 00000000..f68b6f73 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + +public class AttributePair { + public String key, value; + + public AttributePair(String key, String value) { + this.key = key; + this.value = value; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java new file mode 100644 index 00000000..8c5bc1d8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BasicTreeNode { + + private static final BasicTreeNode[] CHILDREN_TEMPLATE = new BasicTreeNode[] {}; + + protected BasicTreeNode mParent; + + protected final List mChildren = new ArrayList(); + + public int x, y, width, height; + + // whether the boundary fields are applicable for the node or not + // RootWindowNode has no bounds, but UiNodes should + protected boolean mHasBounds = false; + + public void addChild(BasicTreeNode child) { + if (child == null) { + throw new NullPointerException("Cannot add null child"); + } + if (mChildren.contains(child)) { + throw new IllegalArgumentException("node already a child"); + } + mChildren.add(child); + child.mParent = this; + } + + public List getChildrenList() { + return Collections.unmodifiableList(mChildren); + } + + public BasicTreeNode[] getChildren() { + return mChildren.toArray(CHILDREN_TEMPLATE); + } + + public BasicTreeNode getParent() { + return mParent; + } + + public boolean hasChild() { + return mChildren.size() != 0; + } + + public int getChildCount() { + return mChildren.size(); + } + + public void clearAllChildren() { + for (BasicTreeNode child : mChildren) { + child.clearAllChildren(); + } + mChildren.clear(); + } + + /** + * + * Find nodes in the tree containing the coordinate + * + * The found node should have bounds covering the coordinate, and none of its children's + * bounds covers it. Depending on the layout, some app may have multiple nodes matching it, + * the caller must provide a {@link IFindNodeListener} to receive all found nodes + * + * @param px + * @param py + * @return + */ + public boolean findLeafMostNodesAtPoint(int px, int py, IFindNodeListener listener) { + boolean foundInChild = false; + for (BasicTreeNode node : mChildren) { + foundInChild |= node.findLeafMostNodesAtPoint(px, py, listener); + } + // checked all children, if at least one child covers the point, return directly + if (foundInChild) return true; + // check self if the node has no children, or no child nodes covers the point + if (mHasBounds) { + if (x <= px && px <= x + width && y <= py && py <= y + height) { + listener.onFoundNode(this); + return true; + } else { + return false; + } + } else { + return false; + } + } + + public Object[] getAttributesArray () { + return null; + }; + + public static interface IFindNodeListener { + void onFoundNode(BasicTreeNode node); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java new file mode 100644 index 00000000..3fcd90d8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +public class BasicTreeNodeContentProvider implements ITreeContentProvider { + + private static final Object[] EMPTY_ARRAY = {}; + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof BasicTreeNode) { + return ((BasicTreeNode)parentElement).getChildren(); + } + return EMPTY_ARRAY; + } + + @Override + public Object getParent(Object element) { + if (element instanceof BasicTreeNode) { + return ((BasicTreeNode)element).getParent(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof BasicTreeNode) { + return ((BasicTreeNode) element).hasChild(); + } + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java new file mode 100644 index 00000000..0d2eae4f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + + + +public class RootWindowNode extends BasicTreeNode { + + private final String mWindowName; + private Object[] mCachedAttributesArray; + private int mRotation; + + public RootWindowNode(String windowName) { + this(windowName, 0); + } + + public RootWindowNode(String windowName, int rotation) { + mWindowName = windowName; + mRotation = rotation; + } + + @Override + public String toString() { + return mWindowName; + } + + @Override + public Object[] getAttributesArray() { + if (mCachedAttributesArray == null) { + mCachedAttributesArray = new Object[]{new AttributePair("window-name", mWindowName)}; + } + return mCachedAttributesArray; + } + + public int getRotation() { + return mRotation; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java new file mode 100644 index 00000000..44124f87 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + +import org.eclipse.swt.graphics.Rectangle; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +public class UiHierarchyXmlLoader { + + private BasicTreeNode mRootNode; + private List mNafNodes; + private List mNodeList; + public UiHierarchyXmlLoader() { + } + + /** + * Uses a SAX parser to process XML dump + * @param xmlPath + * @return + */ + public BasicTreeNode parseXml(String xmlPath) { + mRootNode = null; + mNafNodes = new ArrayList(); + mNodeList = new ArrayList(); + // standard boilerplate to get a SAX parser + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser parser = null; + try { + parser = factory.newSAXParser(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return null; + } catch (SAXException e) { + e.printStackTrace(); + return null; + } + // handler class for SAX parser to receiver standard parsing events: + // e.g. on reading "", startElement is called, on reading "", + // endElement is called + DefaultHandler handler = new DefaultHandler(){ + BasicTreeNode mParentNode; + BasicTreeNode mWorkingNode; + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + boolean nodeCreated = false; + // starting an element implies that the element that has not yet been closed + // will be the parent of the element that is being started here + mParentNode = mWorkingNode; + if ("hierarchy".equals(qName)) { + int rotation = 0; + for (int i = 0; i < attributes.getLength(); i++) { + if ("rotation".equals(attributes.getQName(i))) { + try { + rotation = Integer.parseInt(attributes.getValue(i)); + } catch (NumberFormatException nfe) { + // do nothing + } + } + } + mWorkingNode = new RootWindowNode(attributes.getValue("windowName"), rotation); + nodeCreated = true; + } else if ("node".equals(qName)) { + UiNode tmpNode = new UiNode(); + for (int i = 0; i < attributes.getLength(); i++) { + tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i)); + } + mWorkingNode = tmpNode; + nodeCreated = true; + // check if current node is NAF + String naf = tmpNode.getAttribute("NAF"); + if ("true".equals(naf)) { + mNafNodes.add(new Rectangle(tmpNode.x, tmpNode.y, + tmpNode.width, tmpNode.height)); + } + } + // nodeCreated will be false if the element started is neither + // "hierarchy" nor "node" + if (nodeCreated) { + if (mRootNode == null) { + // this will only happen once + mRootNode = mWorkingNode; + } + if (mParentNode != null) { + mParentNode.addChild(mWorkingNode); + mNodeList.add(mWorkingNode); + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + //mParentNode should never be null here in a well formed XML + if (mParentNode != null) { + // closing an element implies that we are back to working on + // the parent node of the element just closed, i.e. continue to + // parse more child nodes + mWorkingNode = mParentNode; + mParentNode = mParentNode.getParent(); + } + } + }; + try { + parser.parse(new File(xmlPath), handler); + } catch (SAXException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return mRootNode; + } + + /** + * Returns the list of "Not Accessibility Friendly" nodes found during parsing. + * + * Call this function after parsing + * + * @return + */ + public List getNafNodes() { + return Collections.unmodifiableList(mNafNodes); + } + + public List getAllNodes(){ + return mNodeList; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java new file mode 100644 index 00000000..5b5db393 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.uiautomator.tree; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UiNode extends BasicTreeNode { + private static final Pattern BOUNDS_PATTERN = Pattern + .compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]"); + // use LinkedHashMap to preserve the order of the attributes + private final Map mAttributes = new LinkedHashMap(); + private String mDisplayName = "ShouldNotSeeMe"; + private Object[] mCachedAttributesArray; + + public void addAtrribute(String key, String value) { + mAttributes.put(key, value); + updateDisplayName(); + if ("bounds".equals(key)) { + updateBounds(value); + } + } + + public Map getAttributes() { + return Collections.unmodifiableMap(mAttributes); + } + + /** + * Builds the display name based on attributes of the node + */ + private void updateDisplayName() { + String className = mAttributes.get("class"); + if (className == null) + return; + String text = mAttributes.get("text"); + if (text == null) + return; + String contentDescription = mAttributes.get("content-desc"); + if (contentDescription == null) + return; + String index = mAttributes.get("index"); + if (index == null) + return; + String bounds = mAttributes.get("bounds"); + if (bounds == null) { + return; + } + // shorten the standard class names, otherwise it takes up too much space on UI + className = className.replace("android.widget.", ""); + className = className.replace("android.view.", ""); + StringBuilder builder = new StringBuilder(); + builder.append('('); + builder.append(index); + builder.append(") "); + builder.append(className); + if (!text.isEmpty()) { + builder.append(':'); + builder.append(text); + } + if (!contentDescription.isEmpty()) { + builder.append(" {"); + builder.append(contentDescription); + builder.append('}'); + } + builder.append(' '); + builder.append(bounds); + mDisplayName = builder.toString(); + } + + private void updateBounds(String bounds) { + Matcher m = BOUNDS_PATTERN.matcher(bounds); + if (m.matches()) { + x = Integer.parseInt(m.group(1)); + y = Integer.parseInt(m.group(2)); + width = Integer.parseInt(m.group(3)) - x; + height = Integer.parseInt(m.group(4)) - y; + mHasBounds = true; + } else { + throw new RuntimeException("Invalid bounds: " + bounds); + } + } + + @Override + public String toString() { + return mDisplayName; + } + + public String getAttribute(String key) { + return mAttributes.get(key); + } + + @Override + public Object[] getAttributesArray() { + // this approach means we do not handle the situation where an attribute is added + // after this function is first called. This is currently not a concern because the + // tree is supposed to be readonly + if (mCachedAttributesArray == null) { + mCachedAttributesArray = new Object[mAttributes.size()]; + int i = 0; + for (String attr : mAttributes.keySet()) { + mCachedAttributesArray[i++] = new AttributePair(attr, mAttributes.get(attr)); + } + } + return mCachedAttributesArray; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png new file mode 100644 index 00000000..f3e53d75 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/expandall.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/expandall.png new file mode 100644 index 00000000..7bdf83d3 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/expandall.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/next.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/next.png new file mode 100644 index 00000000..37481696 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/next.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png new file mode 100644 index 00000000..8c4a2e1b Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/prev.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/prev.png new file mode 100644 index 00000000..e4834a26 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/prev.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/save.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/save.png new file mode 100644 index 00000000..5f668644 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/save.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshot.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshot.png new file mode 100644 index 00000000..423f7817 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshot.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshotcompressed.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshotcompressed.png new file mode 100644 index 00000000..872f3562 Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshotcompressed.png differ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/warning.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/warning.png new file mode 100644 index 00000000..ca3b6ede Binary files /dev/null and b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/warning.png differ diff --git a/andmore-swt/pom.xml b/andmore-swt/pom.xml new file mode 100644 index 00000000..5636932f --- /dev/null +++ b/andmore-swt/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + org.eclipse.andmore + andmore-parent + 0.5.2-SNAPSHOT + + + swt-droid-parent + SWT Android Parent + pom + + + + UTF-8 + UTF-8 + + + + org.eclipse.andmore.swt + org.eclipse.andmore.swtmenubar + org.eclipse.andmore.ddmuilib + org.eclipse.andmore.sdkuilib + org.eclipse.andmore.sdkstats + org.eclipse.andmore.traceviewuilib + org.eclipse.andmore.hierarchyviewer2lib + org.eclipse.andmore.uiautomatorviewer + features/org.eclipse.andmore.swt + features/org.eclipse.andmore.swtmenubar + features/org.eclipse.andmore.ddmuilib + features/org.eclipse.andmore.sdkuilib + features/org.eclipse.andmore.sdkstats + features/org.eclipse.andmore.traceviewuilib + features/org.eclipse.andmore.hierarchyviewer2lib + features/org.eclipse.andmore.uiautomatorviewer + + diff --git a/andmore.target/.gitignore b/andmore.target/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore.target/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore.target/.project b/andmore.target/.project new file mode 100644 index 00000000..a894551a --- /dev/null +++ b/andmore.target/.project @@ -0,0 +1,17 @@ + + + andmore.target + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/andmore.target/.settings/org.eclipse.m2e.core.prefs b/andmore.target/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore.target/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore.target/andmore.target.target b/andmore.target/andmore.target.target new file mode 100644 index 00000000..bcbee37b --- /dev/null +++ b/andmore.target/andmore.target.target @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/andmore.target/pom.xml b/andmore.target/pom.xml new file mode 100644 index 00000000..d1cfd091 --- /dev/null +++ b/andmore.target/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + org.eclipse.andmore + andmore-parent + 0.5.2-SNAPSHOT + + andmore.target + eclipse-target-definition + \ No newline at end of file diff --git a/android-core/features/org.eclipse.andmore.package/feature.xml b/android-core/features/org.eclipse.andmore.package/feature.xml index edc5a300..bf96ac2f 100644 --- a/android-core/features/org.eclipse.andmore.package/feature.xml +++ b/android-core/features/org.eclipse.andmore.package/feature.xml @@ -1,46 +1,54 @@ - - - - - Android Developer Tools - - - - Copyright (C) 2007-2014 The Android Open Source Project - - - - %license - - - - - - - - - - - - - - - - - - - - - - + + + + + Android Developer Tools + + + + Copyright (C) 2007-2014 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-core/features/org.eclipse.andmore/feature.xml b/android-core/features/org.eclipse.andmore/feature.xml index 8d209d87..895806e3 100644 --- a/android-core/features/org.eclipse.andmore/feature.xml +++ b/android-core/features/org.eclipse.andmore/feature.xml @@ -1,120 +1,177 @@ - - - - - %feature.description - - - - %feature.copyright - - - - %license - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + %feature.description + + + + %feature.copyright + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.base/.classpath b/android-core/plugins/org.eclipse.andmore.base/.classpath index 62c0fbc5..7498423d 100644 --- a/android-core/plugins/org.eclipse.andmore.base/.classpath +++ b/android-core/plugins/org.eclipse.andmore.base/.classpath @@ -1,17 +1,7 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +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=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF index ab73938d..831e9cb8 100644 --- a/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF @@ -1,89 +1,17 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Common Android Utilities -Bundle-SymbolicName: org.eclipse.andmore.base;singleton:=true -Bundle-Version: 0.5.2.qualifier -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.apache.httpcomponents.httpclient;bundle-version="4.1.3", - org.apache.httpcomponents.httpcore;bundle-version="4.1.4", - org.apache.commons.logging;bundle-version="1.1.1", - org.apache.commons.codec;bundle-version="1.4.0", - org.apache.commons.compress;bundle-version="1.6.0", - com.google.gson;bundle-version="2.2.4" -Bundle-ActivationPolicy: lazy -Bundle-Vendor: Eclipse Andmore -Bundle-ClassPath: ., - libs/annotations.jar, - libs/common.jar, - libs/guava-17.0.jar, - libs/httpmime-4.1.jar, - libs/kxml2-2.3.0.jar, - libs/layoutlib-api.jar, - libs/sdklib.jar, - libs/sdkstats.jar, - libs/dvlib.jar, - libs/sdk-common.jar -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: com.android, - com.android.annotations, - com.android.annotations.concurrency, - com.android.dvlib, - com.android.ide.common.internal, - com.android.ide.common.packaging, - com.android.ide.common.rendering, - com.android.ide.common.rendering.api, - com.android.ide.common.rendering.legacy, - com.android.ide.common.res2, - com.android.ide.common.resources, - com.android.ide.common.resources.configuration, - com.android.ide.common.sdk, - com.android.ide.common.util, - com.android.ide.common.xml, - com.android.io, - com.android.layoutlib.api, - com.android.prefs, - com.android.resources, - com.android.sdklib, - com.android.sdklib.build, - com.android.sdklib.devices, - com.android.sdklib.internal.avd, - com.android.sdklib.internal.build, - com.android.sdklib.internal.project, - com.android.sdklib.internal.repository, - com.android.sdklib.internal.repository.archives, - com.android.sdklib.internal.repository.packages, - com.android.sdklib.internal.repository.sources, - com.android.sdklib.internal.repository.updater, - com.android.sdklib.io, - com.android.sdklib.repository, - com.android.sdklib.repository.descriptors, - com.android.sdklib.repository.local, - com.android.sdklib.util, - com.android.sdkstats, - com.android.util, - com.android.utils, - com.android.xml, - com.google.common.annotations, - com.google.common.base, - com.google.common.base.internal, - com.google.common.cache, - com.google.common.collect, - com.google.common.eventbus, - com.google.common.hash, - com.google.common.io, - com.google.common.math, - com.google.common.net, - com.google.common.primitives, - com.google.common.reflect, - com.google.common.util.concurrent, - org.apache.http.entity.mime, - org.apache.http.entity.mime.content, - org.eclipse.andmore.base, - org.kxml2.io, - org.kxml2.kdom, - org.kxml2.wap, - org.kxml2.wap.syncml, - org.kxml2.wap.wml, - org.kxml2.wap.wv, - org.xmlpull.v1 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Common Android Utilities +Bundle-SymbolicName: org.eclipse.andmore.base;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.apache.commons.logging;bundle-version="1.1.1", + org.apache.commons.codec;bundle-version="1.4.0", + org.apache.commons.compress;bundle-version="1.6.0", + com.google.gson;bundle-version="2.2.4" +Bundle-ActivationPolicy: lazy +Bundle-Vendor: Eclipse Andmore +Bundle-ClassPath: . +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.base/build.properties b/android-core/plugins/org.eclipse.andmore.base/build.properties index 6ffb5552..3a71c2a3 100644 --- a/android-core/plugins/org.eclipse.andmore.base/build.properties +++ b/android-core/plugins/org.eclipse.andmore.base/build.properties @@ -1,9 +1,8 @@ -output.. = bin/ -bin.includes = .,\ - libs/,\ - META-INF/,\ - plugin.xml,\ - plugin.properties,\ - about.html -jars.compile.order = . -source.. = src/ +output.. = bin/ +bin.includes = .,\ + META-INF/,\ + plugin.xml,\ + plugin.properties,\ + about.html +jars.compile.order = . +source.. = src/ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar b/android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar deleted file mode 100644 index 4010e30a..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/common.jar b/android-core/plugins/org.eclipse.andmore.base/libs/common.jar deleted file mode 100644 index bbcf3dde..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/common.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar b/android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar deleted file mode 100644 index 842d87e3..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/guava-17.0.jar b/android-core/plugins/org.eclipse.andmore.base/libs/guava-17.0.jar deleted file mode 100644 index 661fc747..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/guava-17.0.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar b/android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar deleted file mode 100644 index 68f61584..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/kxml2-2.3.0.jar b/android-core/plugins/org.eclipse.andmore.base/libs/kxml2-2.3.0.jar deleted file mode 100644 index 64709522..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/kxml2-2.3.0.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/layoutlib-api.jar b/android-core/plugins/org.eclipse.andmore.base/libs/layoutlib-api.jar deleted file mode 100644 index ae1d8245..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/layoutlib-api.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/sdk-common.jar b/android-core/plugins/org.eclipse.andmore.base/libs/sdk-common.jar deleted file mode 100644 index b041a023..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/sdk-common.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/sdklib.jar b/android-core/plugins/org.eclipse.andmore.base/libs/sdklib.jar deleted file mode 100644 index 4325ef6b..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/sdklib.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/sdkstats.jar b/android-core/plugins/org.eclipse.andmore.base/libs/sdkstats.jar deleted file mode 100644 index 2b7fa9d0..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.base/libs/sdkstats.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/.classpath b/android-core/plugins/org.eclipse.andmore.ddms/.classpath index 5819809d..7498423d 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/.classpath +++ b/android-core/plugins/org.eclipse.andmore.ddms/.classpath @@ -1,10 +1,7 @@ - - - - - - - - - - + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.ddms/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.ddms/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.ddms/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +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=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.ddms/META-INF/MANIFEST.MF index 16b45f39..31d7dddf 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.ddms/META-INF/MANIFEST.MF @@ -1,40 +1,24 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.ddms;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.ddms.DdmsPlugin -Bundle-Vendor: %Bundle-Vendor -Bundle-Localization: plugin -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.console, - org.eclipse.core.resources, - org.eclipse.ui.ide, - org.eclipse.core.filesystem, - org.eclipse.andmore.base -Bundle-ActivationPolicy: lazy -Export-Package: com.android.ddmlib, - com.android.ddmlib.log, - com.android.ddmlib.testrunner, - com.android.ddmlib.utils, - com.android.ddmuilib, - com.android.ddmuilib.actions, - com.android.ddmuilib.annotation, - com.android.ddmuilib.console, - com.android.ddmuilib.explorer, - com.android.ddmuilib.handler, - com.android.ddmuilib.heap, - com.android.ddmuilib.location, - com.android.ddmuilib.log.event, - com.android.ddmuilib.logcat, - com.android.ddmuilib.net, - org.eclipse.andmore.ddms, - org.eclipse.andmore.ddms.i18n, - org.eclipse.andmore.ddms.preferences, - org.eclipse.andmore.ddms.views -Bundle-ClassPath: ., - libs/ddmlib.jar, - libs/ddmuilib.jar, - libs/uiautomatorviewer.jar -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.ddms;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.ddms.DdmsPlugin +Bundle-Vendor: %Bundle-Vendor +Bundle-Localization: plugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.console, + org.eclipse.core.resources, + org.eclipse.ui.ide, + org.eclipse.core.filesystem, + org.eclipse.andmore.swt, + org.eclipse.andmore.ddmuilib, + org.eclipse.andmore.uiautomatorviewer +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.ddms, + org.eclipse.andmore.ddms.i18n, + org.eclipse.andmore.ddms.preferences, + org.eclipse.andmore.ddms.views +Bundle-ClassPath: . +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/build.properties b/android-core/plugins/org.eclipse.andmore.ddms/build.properties index 3c02154c..0122c63c 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/build.properties +++ b/android-core/plugins/org.eclipse.andmore.ddms/build.properties @@ -1,13 +1,14 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - icons/,\ - plugin.xml,\ - .,\ - libs/,\ - schema/,\ - about.html,\ - about.ini,\ - about.properties,\ - plugin.properties - +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + icons/,\ + plugin.xml,\ + .,\ + schema/,\ + about.html,\ + about.ini,\ + about.properties,\ + plugin.properties,\ + ddm/icons/,\ + sdk/ + diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/add.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/add.png new file mode 100644 index 00000000..eefc2ca3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/add.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/android.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/android.png new file mode 100644 index 00000000..3779d4d3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/android.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/backward.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/backward.png new file mode 100644 index 00000000..90a97137 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/backward.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/capture.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/capture.png new file mode 100644 index 00000000..da5c10be Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/capture.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/clear.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/clear.png new file mode 100644 index 00000000..0009cf66 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/clear.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/d.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/d.png new file mode 100644 index 00000000..d45506ee Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/d.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-attach.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-attach.png new file mode 100644 index 00000000..9b8a11c4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-attach.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-error.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-error.png new file mode 100644 index 00000000..f22da1ff Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-error.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-wait.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-wait.png new file mode 100644 index 00000000..322be632 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/debug-wait.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/delete.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/delete.png new file mode 100644 index 00000000..db5fab8e Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/delete.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/device.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/device.png new file mode 100644 index 00000000..7dbbbb6a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/device.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/diff.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/diff.png new file mode 100644 index 00000000..bdd9e5c1 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/diff.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/displayfilters.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/displayfilters.png new file mode 100644 index 00000000..d110c2cf Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/displayfilters.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/down.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/down.png new file mode 100644 index 00000000..f9426cba Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/down.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/e.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/e.png new file mode 100644 index 00000000..dee7c97f Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/e.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/edit.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/edit.png new file mode 100644 index 00000000..b8f65bcd Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/edit.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/empty.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/empty.png new file mode 100644 index 00000000..f0215424 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/empty.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/emulator.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/emulator.png new file mode 100644 index 00000000..a7180428 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/emulator.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/file.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/file.png new file mode 100644 index 00000000..043a8143 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/file.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/folder.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/folder.png new file mode 100644 index 00000000..7e29b1a4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/folder.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/forward.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/forward.png new file mode 100644 index 00000000..a97a6056 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/forward.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/gc.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/gc.png new file mode 100644 index 00000000..51948064 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/gc.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/groupby.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/groupby.png new file mode 100644 index 00000000..250b9827 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/groupby.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/halt.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/halt.png new file mode 100644 index 00000000..10e3720a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/halt.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/heap.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/heap.png new file mode 100644 index 00000000..e3aa3f06 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/heap.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/hprof.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/hprof.png new file mode 100644 index 00000000..123d0620 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/hprof.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/i.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/i.png new file mode 100644 index 00000000..98385c51 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/i.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/importBug.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/importBug.png new file mode 100644 index 00000000..f5da179c Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/importBug.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/load.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/load.png new file mode 100644 index 00000000..9e7bf6e7 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/load.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pause.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pause.png new file mode 100644 index 00000000..19d286d3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pause.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/play.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/play.png new file mode 100644 index 00000000..d54f013f Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/play.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pull.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pull.png new file mode 100644 index 00000000..f48f1b1f Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/pull.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/push.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/push.png new file mode 100644 index 00000000..6222864c Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/push.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/save.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/save.png new file mode 100644 index 00000000..040ebda6 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/save.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/scroll_lock.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/scroll_lock.png new file mode 100644 index 00000000..5d26689b Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/scroll_lock.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_down.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_down.png new file mode 100644 index 00000000..2d4ccc1a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_down.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_up.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_up.png new file mode 100644 index 00000000..3a0bc3cb Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/sort_up.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/thread.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/thread.png new file mode 100644 index 00000000..ac839e89 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/thread.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_start.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_start.png new file mode 100644 index 00000000..88771cc6 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_start.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_stop.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_stop.png new file mode 100644 index 00000000..71bd215f Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/tracing_stop.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/up.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/up.png new file mode 100644 index 00000000..92edf5a8 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/up.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/v.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/v.png new file mode 100644 index 00000000..80440515 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/v.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/w.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/w.png new file mode 100644 index 00000000..129d0f9c Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/w.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/warning.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/warning.png new file mode 100644 index 00000000..ca3b6ede Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/warning.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/zygote.png b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/zygote.png new file mode 100644 index 00000000..5cbb1d26 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/zygote.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmlib.jar b/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmlib.jar deleted file mode 100644 index ccc1f72f..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmlib.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar b/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar deleted file mode 100644 index 01e4079b..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar b/android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar deleted file mode 100644 index ebef827d..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png new file mode 100644 index 00000000..ae61f7df Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png new file mode 100644 index 00000000..addef8ef Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png new file mode 100644 index 00000000..830c04b0 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png new file mode 100644 index 00000000..08ffda85 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/archive_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/archive_icon16.png new file mode 100644 index 00000000..be5edd79 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/archive_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_16.png new file mode 100644 index 00000000..945d871b Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png new file mode 100644 index 00000000..6daa67b8 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/buildtool_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/buildtool_pkg_16.png new file mode 100644 index 00000000..9b917da4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/buildtool_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/device.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/device.png new file mode 100644 index 00000000..7dbbbb6a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/device.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png new file mode 100644 index 00000000..a2be37af Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/emulator.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/emulator.png new file mode 100644 index 00000000..a7180428 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/emulator.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png new file mode 100644 index 00000000..ccb4d0aa Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png new file mode 100644 index 00000000..7ad8a669 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/incompat_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/incompat_icon16.png new file mode 100644 index 00000000..2a307e92 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/incompat_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png new file mode 100644 index 00000000..ad2edff6 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png new file mode 100644 index 00000000..ed27b520 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/nopkg_icon_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/nopkg_icon_16.png new file mode 100644 index 00000000..147837fd Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/nopkg_icon_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_incompat_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_incompat_16.png new file mode 100644 index 00000000..d7d3ae6e Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_incompat_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png new file mode 100644 index 00000000..70295655 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_new_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_new_16.png new file mode 100644 index 00000000..9c93afc8 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_new_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png new file mode 100644 index 00000000..4171ba63 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_16.png new file mode 100644 index 00000000..0ee32bf8 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png new file mode 100644 index 00000000..395a2403 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png new file mode 100644 index 00000000..56e10d0a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platformtool_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platformtool_pkg_16.png new file mode 100644 index 00000000..424fb295 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platformtool_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/reject_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/reject_icon16.png new file mode 100644 index 00000000..18c14811 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/reject_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sample_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sample_pkg_16.png new file mode 100644 index 00000000..11210bae Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sample_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sdkman_logo_128.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sdkman_logo_128.png new file mode 100644 index 00000000..0f1670d7 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sdkman_logo_128.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/source_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/source_pkg_16.png new file mode 100644 index 00000000..ab08d29a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/source_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png new file mode 100644 index 00000000..ae61f7df Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png new file mode 100644 index 00000000..721d1841 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_enabled_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_enabled_16.png new file mode 100644 index 00000000..198f2998 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_enabled_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png new file mode 100644 index 00000000..942ce47a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png new file mode 100644 index 00000000..8887bcd3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_32.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_32.png new file mode 100644 index 00000000..13fed144 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_32.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png new file mode 100644 index 00000000..652c5efa Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png new file mode 100644 index 00000000..33560163 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png new file mode 100644 index 00000000..e3ad2e60 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png new file mode 100644 index 00000000..f4264e7b Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tool_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tool_pkg_16.png new file mode 100644 index 00000000..424fb295 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tool_pkg_16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/unknown_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/unknown_icon16.png new file mode 100644 index 00000000..2b255fa9 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/unknown_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png new file mode 100644 index 00000000..a2fcf7cf Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmResourceProvider.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmResourceProvider.java new file mode 100644 index 00000000..2ad96795 --- /dev/null +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmResourceProvider.java @@ -0,0 +1,13 @@ +package org.eclipse.andmore.ddms; + +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.jface.resource.ImageDescriptor; + +public class DdmResourceProvider implements PluginResourceProvider { + + @Override + public ImageDescriptor descriptorFromPath(String imagePath) { + return DdmsPlugin.getImageDescriptor("ddm/" + imagePath); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmsPlugin.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmsPlugin.java index 0905eefc..ff00cc7b 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmsPlugin.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmsPlugin.java @@ -1,924 +1,935 @@ -/* - * 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 org.eclipse.andmore.ddms; - -import com.android.annotations.NonNull; -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; -import com.android.ddmlib.Client; -import com.android.ddmlib.DdmPreferences; -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.ddmuilib.DdmUiPreferences; -import com.android.ddmuilib.DevicePanel.IUiSelectionListener; -import com.android.ddmuilib.StackTracePanel; -import com.android.ddmuilib.console.DdmConsole; -import com.android.ddmuilib.console.IDdmConsole; - -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IConfigurationElement; -import org.eclipse.core.runtime.IExtensionPoint; -import org.eclipse.core.runtime.IExtensionRegistry; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.util.IPropertyChangeListener; -import org.eclipse.jface.util.PropertyChangeEvent; -import org.eclipse.swt.SWTException; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.console.ConsolePlugin; -import org.eclipse.ui.console.IConsole; -import org.eclipse.ui.console.MessageConsole; -import org.eclipse.ui.console.MessageConsoleStream; -import org.eclipse.ui.plugin.AbstractUIPlugin; -import org.osgi.framework.BundleContext; - -import java.io.File; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.List; - -/** - * The activator class controls the plug-in life cycle - */ -public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener, IUiSelectionListener, - com.android.ddmuilib.StackTracePanel.ISourceRevealer { - - // The plug-in ID - public static final String PLUGIN_ID = "org.eclipse.andmore.ddms"; //$NON-NLS-1$ - - public static final String NEWLINE = System.getProperty("line.separator"); - - /** The singleton instance */ - private static DdmsPlugin sPlugin; - - /** Location of the adb command line executable */ - private static String sAdbLocation; - private static String sToolsFolder; - private static String sHprofConverter; - - private boolean mHasDebuggerConnectors; - /** - * debugger connectors for already running apps. Initialized from an - * extension point. - */ - private IDebuggerConnector[] mDebuggerConnectors; - private ITraceviewLauncher[] mTraceviewLaunchers; - private List mClientSpecificActions = null; - - /** Console for DDMS log message */ - private MessageConsole mDdmsConsole; - - private IDevice mCurrentDevice; - private Client mCurrentClient; - private boolean mListeningToUiSelection = false; - - private final ArrayList mListeners = new ArrayList(); - - private Color mRed; - - /** - * Classes which implement this interface provide methods that deals with - * {@link IDevice} and {@link Client} selectionchanges. - */ - public interface ISelectionListener { - - /** - * Sent when a new {@link Client} is selected. - * - * @param selectedClient - * The selected client. If null, no clients are selected. - */ - public void selectionChanged(Client selectedClient); - - /** - * Sent when a new {@link IDevice} is selected. - * - * @param selectedDevice - * the selected device. If null, no devices are selected. - */ - public void selectionChanged(IDevice selectedDevice); - } - - /** - * The constructor - */ - public DdmsPlugin() { - sPlugin = this; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext - * ) - */ - @Override - public void start(BundleContext context) throws Exception { - super.start(context); - - final Display display = getDisplay(); - - // get the eclipse store - final IPreferenceStore eclipseStore = getPreferenceStore(); - - AndroidDebugBridge.addDeviceChangeListener(this); - - DdmUiPreferences.setStore(eclipseStore); - - // DdmUiPreferences.displayCharts(); - - // set the consoles. - mDdmsConsole = new MessageConsole("DDMS", null); //$NON-NLS-1$ - ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { mDdmsConsole }); - - final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); - final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); - mRed = new Color(display, 0xFF, 0x00, 0x00); - - // because this can be run, in some cases, by a non UI thread, and - // because - // changing the console properties update the UI, we need to make this - // change - // in the UI thread. - display.asyncExec(new Runnable() { - @Override - public void run() { - errorConsoleStream.setColor(mRed); - } - }); - - // set up the ddms log to use the ddms console. - Log.setLogOutput(new ILogOutput() { - @Override - public void printLog(LogLevel logLevel, String tag, String message) { - if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { - printToStream(errorConsoleStream, tag, message); - showConsoleView(mDdmsConsole); - } else { - printToStream(consoleStream, tag, message); - } - } - - @Override - public void printAndPromptLog(final LogLevel logLevel, final String tag, final String message) { - printLog(logLevel, tag, message); - // dialog box only run in UI thread.. - display.asyncExec(new Runnable() { - @Override - public void run() { - Shell shell = display.getActiveShell(); - if (logLevel == LogLevel.ERROR) { - MessageDialog.openError(shell, tag, message); - } else { - MessageDialog.openWarning(shell, tag, message); - } - } - }); - } - - }); - - // set up the ddms console to use this objects - DdmConsole.setConsole(new IDdmConsole() { - @Override - public void printErrorToConsole(String message) { - printToStream(errorConsoleStream, null, message); - showConsoleView(mDdmsConsole); - } - - @Override - public void printErrorToConsole(String[] messages) { - for (String m : messages) { - printToStream(errorConsoleStream, null, m); - } - showConsoleView(mDdmsConsole); - } - - @Override - public void printToConsole(String message) { - printToStream(consoleStream, null, message); - } - - @Override - public void printToConsole(String[] messages) { - for (String m : messages) { - printToStream(consoleStream, null, m); - } - } - }); - - // set the listener for the preference change - eclipseStore.addPropertyChangeListener(new IPropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent event) { - // get the name of the property that changed. - String property = event.getProperty(); - - if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { - DdmPreferences.setDebugPortBase(eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); - } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { - DdmPreferences.setSelectedDebugPort(eclipseStore - .getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); - } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { - DdmUiPreferences.setThreadRefreshInterval(eclipseStore - .getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); - } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { - DdmPreferences.setLogLevel(eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); - } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) { - DdmPreferences.setTimeOut(eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT)); - } else if (PreferenceInitializer.ATTR_USE_ADBHOST.equals(property)) { - DdmPreferences.setUseAdbHost(eclipseStore.getBoolean(PreferenceInitializer.ATTR_USE_ADBHOST)); - } else if (PreferenceInitializer.ATTR_ADBHOST_VALUE.equals(property)) { - DdmPreferences.setAdbHostValue(eclipseStore.getString(PreferenceInitializer.ATTR_ADBHOST_VALUE)); - } - } - }); - - // do some last initializations - - // set the preferences. - PreferenceInitializer.setupPreferences(); - - // this class is set as the main source revealer and will look at all - // the implementations - // of the extension point. see #reveal(String, String, int) - StackTracePanel.setSourceRevealer(this); - - /* - * Load the extension point implementations. The first step is to load - * the IConfigurationElement representing the implementations. The 2nd - * step is to use these objects to instantiate the implementation - * classes. - * - * Because the 2nd step will trigger loading the plug-ins providing the - * implementations, and those plug-ins could access DDMS classes (like - * ADT), this 2nd step should be done in a Job to ensure that DDMS is - * loaded, so that the other plug-ins can load. - * - * Both steps could be done in the 2nd step but some of DDMS UI rely on - * knowing if there is an implementation or not (DeviceView), so we do - * the first steps in start() and, in some case, record it. - */ - - // get the IConfigurationElement for the debuggerConnector right away. - final IConfigurationElement[] dcce = findConfigElements("org.eclipse.andmore.ddms.debuggerConnector"); //$NON-NLS-1$ - mHasDebuggerConnectors = dcce.length > 0; - - // get the other configElements and instantiante them in a Job. - new Job(Messages.DdmsPlugin_DDMS_Post_Create_Init) { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - // init the lib - AndroidDebugBridge.init(true /* debugger support */); - - // get the available adb locators - IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.toolsLocator"); //$NON-NLS-1$ - - IToolsLocator[] locators = instantiateToolsLocators(elements); - - for (IToolsLocator locator : locators) { - try { - String adbLocation = locator.getAdbLocation(); - String traceviewLocation = locator.getTraceViewLocation(); - String hprofConvLocation = locator.getHprofConvLocation(); - if (adbLocation != null && traceviewLocation != null && hprofConvLocation != null) { - // checks if the location is valid. - if (setToolsLocation(adbLocation, hprofConvLocation, traceviewLocation)) { - - AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); - - // no need to look at the other locators. - break; - } - } - } catch (Throwable t) { - // ignore, we'll just not use this implementation. - } - } - - // get the available debugger connectors - mDebuggerConnectors = instantiateDebuggerConnectors(dcce); - - // get the available Traceview Launchers. - elements = findConfigElements("org.eclipse.andmore.ddms.traceviewLauncher"); //$NON-NLS-1$ - mTraceviewLaunchers = instantiateTraceviewLauncher(elements); - - return Status.OK_STATUS; - } catch (CoreException e) { - return e.getStatus(); - } - } - }.schedule(); - } - - private void showConsoleView(MessageConsole console) { - ConsolePlugin.getDefault().getConsoleManager().showConsoleView(console); - } - - /** - * Obtain a list of configuration elements that extend the given extension - * point. - */ - IConfigurationElement[] findConfigElements(String extensionPointId) { - // get the adb location from an implementation of the ADB Locator - // extension point. - IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry(); - IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint(extensionPointId); - if (extensionPoint != null) { - return extensionPoint.getConfigurationElements(); - } - - // shouldn't happen or it means the plug-in is broken. - return new IConfigurationElement[0]; - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called adbLocator. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private IToolsLocator[] instantiateToolsLocators(IConfigurationElement[] configElements) throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof IToolsLocator) { - list.add((IToolsLocator) obj); - } - } - - return list.toArray(new IToolsLocator[list.size()]); - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called debuggerConnector. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private IDebuggerConnector[] instantiateDebuggerConnectors(IConfigurationElement[] configElements) - throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof IDebuggerConnector) { - list.add((IDebuggerConnector) obj); - } - } - - return list.toArray(new IDebuggerConnector[list.size()]); - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called traceviewLauncher. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private ITraceviewLauncher[] instantiateTraceviewLauncher(IConfigurationElement[] configElements) - throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof ITraceviewLauncher) { - list.add((ITraceviewLauncher) obj); - } - } - - return list.toArray(new ITraceviewLauncher[list.size()]); - } - - /** - * Returns the classes that implement {@link IClientAction} in each of the - * extensions that extend clientAction extension point. - * - * @throws CoreException - */ - private List instantiateClientSpecificActions(IConfigurationElement[] elements) throws CoreException { - if (elements == null || elements.length == 0) { - return Collections.emptyList(); - } - - List extensions = new ArrayList(1); - - for (IConfigurationElement e : elements) { - Object o = e.createExecutableExtension("class"); //$NON-NLS-1$ - if (o instanceof IClientAction) { - extensions.add((IClientAction) o); - } - } - - return extensions; - } - - public static Display getDisplay() { - IWorkbench bench = sPlugin.getWorkbench(); - if (bench != null) { - return bench.getDisplay(); - } - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext - * ) - */ - @Override - public void stop(BundleContext context) throws Exception { - AndroidDebugBridge.removeDeviceChangeListener(this); - - AndroidDebugBridge.terminate(); - - mRed.dispose(); - - sPlugin = null; - super.stop(context); - } - - /** - * Returns the shared instance - * - * @return the shared instance - */ - public static DdmsPlugin getDefault() { - return sPlugin; - } - - public static String getAdb() { - return sAdbLocation; - } - - public static File getPlatformToolsFolder() { - return new File(sAdbLocation).getParentFile(); - } - - public static String getToolsFolder() { - return sToolsFolder; - } - - public static String getHprofConverter() { - return sHprofConverter; - } - - /** - * Stores the adb location. This returns true if the location is an existing - * file. - */ - private static boolean setToolsLocation(String adbLocation, String hprofConvLocation, String traceViewLocation) { - - File adb = new File(adbLocation); - File hprofConverter = new File(hprofConvLocation); - File traceview = new File(traceViewLocation); - - String missing = ""; - if (adb.isFile() == false) { - missing += adb.getAbsolutePath() + " "; - } - if (hprofConverter.isFile() == false) { - missing += hprofConverter.getAbsolutePath() + " "; - } - if (traceview.isFile() == false) { - missing += traceview.getAbsolutePath() + " "; - } - - if (missing.length() > 0) { - String msg = String.format("DDMS files not found: %1$s", missing); - Log.e("DDMS", msg); - Status status = new Status(IStatus.ERROR, PLUGIN_ID, msg, null /* exception */); - getDefault().getLog().log(status); - return false; - } - - sAdbLocation = adbLocation; - sHprofConverter = hprofConverter.getAbsolutePath(); - DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath()); - - sToolsFolder = traceview.getParent(); - - return true; - } - - /** - * Set the location of the adb executable and optionally starts adb - * - * @param adb - * location of adb - * @param startAdb - * flag to start adb - */ - public static void setToolsLocation(String adbLocation, boolean startAdb, String hprofConvLocation, - String traceViewLocation) { - - if (setToolsLocation(adbLocation, hprofConvLocation, traceViewLocation)) { - // starts the server in a thread in case this is blocking. - if (startAdb) { - new Thread() { - @Override - public void run() { - // create and start the bridge - try { - AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */); - } catch (Throwable t) { - Status status = new Status(IStatus.ERROR, PLUGIN_ID, "Failed to create AndroidDebugBridge", - t); - getDefault().getLog().log(status); - } - } - }.start(); - } - } - } - - /** - * Returns whether there are implementations of the debuggerConnectors - * extension point. - *

- * This is guaranteed to return the correct value as soon as the plug-in is - * loaded. - */ - public boolean hasDebuggerConnectors() { - return mHasDebuggerConnectors; - } - - /** - * Returns the implementations of {@link IDebuggerConnector}. - *

- * There may be a small amount of time right after the plug-in load where - * this can return null even if there are implementation. - *

- * Since the use of the implementation likely require user input, the UI can - * use {@link #hasDebuggerConnectors()} to know if there are implementations - * before they are loaded. - */ - public IDebuggerConnector[] getDebuggerConnectors() { - return mDebuggerConnectors; - } - - public synchronized void addSelectionListener(ISelectionListener listener) { - mListeners.add(listener); - - // notify the new listener of the current selection - listener.selectionChanged(mCurrentDevice); - listener.selectionChanged(mCurrentClient); - } - - public synchronized void removeSelectionListener(ISelectionListener listener) { - mListeners.remove(listener); - } - - public synchronized void setListeningState(boolean state) { - mListeningToUiSelection = state; - } - - /** - * Sent when the a device is connected to the {@link AndroidDebugBridge}. - *

- * This is sent from a non UI thread. - * - * @param device - * the new device. - * - * @see IDeviceChangeListener#deviceConnected(IDevice) - */ - @Override - public void deviceConnected(IDevice device) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - if (mCurrentDevice == null) { - handleDefaultSelection(device); - } - } - } - - /** - * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. - *

- * This is sent from a non UI thread. - * - * @param device - * the new device. - * - * @see IDeviceChangeListener#deviceDisconnected(IDevice) - */ - @Override - public void deviceDisconnected(IDevice device) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - // test if the disconnected device was the default selection. - if (mCurrentDevice == device) { - // try to find a new device - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - // get the device list - IDevice[] devices = bridge.getDevices(); - - // check if we still have devices - if (devices.length == 0) { - handleDefaultSelection((IDevice) null); - } else { - handleDefaultSelection(devices[0]); - } - } else { - handleDefaultSelection((IDevice) null); - } - } - } - } - - /** - * Sent when a device data changed, or when clients are started/terminated - * on the device. - *

- * This is sent from a non UI thread. - * - * @param device - * the device that was updated. - * @param changeMask - * the mask indicating what changed. - * - * @see IDeviceChangeListener#deviceChanged(IDevice) - */ - @Override - public void deviceChanged(IDevice device, int changeMask) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - - // check if this is our device - if (device == mCurrentDevice) { - if (mCurrentClient == null) { - handleDefaultSelection(device); - } else { - // get the clients and make sure ours is still in there. - Client[] clients = device.getClients(); - boolean foundClient = false; - for (Client client : clients) { - if (client == mCurrentClient) { - foundClient = true; - break; - } - } - - // if we haven't found our client, lets look for a new one - if (foundClient == false) { - mCurrentClient = null; - handleDefaultSelection(device); - } - } - } - } - } - - /** - * 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. - */ - @Override - public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) { - if (mCurrentDevice != selectedDevice) { - mCurrentDevice = selectedDevice; - - // notify of the new default device - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentDevice); - } - } - - if (mCurrentClient != selectedClient) { - mCurrentClient = selectedClient; - - // notify of the new default client - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentClient); - } - } - } - - /** - * Handles a default selection of a {@link IDevice} and {@link Client}. - * - * @param device - * the selected device - */ - private void handleDefaultSelection(final IDevice device) { - // because the listener expect to receive this from the UI thread, and - // this is called - // from the AndroidDebugBridge notifications, we need to run this in the - // UI thread. - try { - Display display = getDisplay(); - - display.asyncExec(new Runnable() { - @Override - public void run() { - // set the new device if different. - boolean newDevice = false; - if (mCurrentDevice != device) { - mCurrentDevice = device; - newDevice = true; - - // notify of the new default device - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentDevice); - } - } - - if (device != null) { - // if this is a device switch or the same device but we - // didn't find a valid - // client the last time, we go look for a client to use - // again. - if (newDevice || mCurrentClient == null) { - // now get the new client - Client[] clients = device.getClients(); - if (clients.length > 0) { - handleDefaultSelection(clients[0]); - } else { - handleDefaultSelection((Client) null); - } - } - } else { - handleDefaultSelection((Client) null); - } - } - }); - } catch (SWTException e) { - // display is disposed. Do nothing since we're quitting anyway. - } - } - - private void handleDefaultSelection(Client client) { - mCurrentClient = client; - - // notify of the new default client - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentClient); - } - } - - /** - * Prints a message, associated with a project to the specified stream - * - * @param stream - * The stream to write to - * @param tag - * The tag associated to the message. Can be null - * @param message - * The message to print. - */ - private static synchronized void printToStream(MessageConsoleStream stream, String tag, String message) { - String dateTag = getMessageTag(tag); - - stream.print(dateTag); - if (!dateTag.endsWith(" ")) { - stream.print(" "); //$NON-NLS-1$ - } - stream.println(message); - } - - /** - * Creates a string containing the current date/time, and the tag - * - * @param tag - * The tag associated to the message. Can be null - * @return The dateTag - */ - private static String getMessageTag(String tag) { - Calendar c = Calendar.getInstance(); - - if (tag == null) { - return String.format(Messages.DdmsPlugin_Message_Tag_Mask_1, c); - } - - return String.format(Messages.DdmsPlugin_Message_Tag_Mask_2, c, tag); - } - - /** - * Implementation of com.android.ddmuilib.StackTracePanel.ISourceRevealer. - */ - @Override - public void reveal(String applicationName, String className, int line) { - JavaSourceRevealer.reveal(applicationName, className, line); - } - - public boolean launchTraceview(String osPath) { - if (mTraceviewLaunchers != null) { - for (ITraceviewLauncher launcher : mTraceviewLaunchers) { - try { - if (launcher.openFile(osPath)) { - return true; - } - } catch (Throwable t) { - // ignore, we'll just not use this implementation. - } - } - } - - return false; - } - - /** - * Returns the list of clients that extend the clientAction extension point. - */ - @NonNull - public synchronized List getClientSpecificActions() { - if (mClientSpecificActions == null) { - // get available client specific action extensions - IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.clientAction"); //$NON-NLS-1$ - try { - mClientSpecificActions = instantiateClientSpecificActions(elements); - } catch (CoreException e) { - mClientSpecificActions = Collections.emptyList(); - } - } - - return mClientSpecificActions; - } - - private LogCatMonitor mLogCatMonitor; - - public void startLogCatMonitor(IDevice device) { - if (mLogCatMonitor == null) { - mLogCatMonitor = new LogCatMonitor(getDebuggerConnectors(), getPreferenceStore()); - } - - mLogCatMonitor.monitorDevice(device); - } - - /** - * Returns an image descriptor for the image file at the given plug-in - * relative path - */ - public static ImageDescriptor getImageDescriptor(String path) { - return imageDescriptorFromPlugin(PLUGIN_ID, path); - } -} +/* + * 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 org.eclipse.andmore.ddms; + +import com.android.annotations.NonNull; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.DdmPreferences; +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.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.StackTracePanel; +import com.android.ddmuilib.console.DdmConsole; +import com.android.ddmuilib.console.IDdmConsole; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +import java.io.File; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +/** + * The activator class controls the plug-in life cycle + */ +public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener, IUiSelectionListener, + com.android.ddmuilib.StackTracePanel.ISourceRevealer { + + // The plug-in ID + public static final String PLUGIN_ID = "org.eclipse.andmore.ddms"; //$NON-NLS-1$ + + public static final String NEWLINE = System.getProperty("line.separator"); + + /** The singleton instance */ + private static DdmsPlugin sPlugin; + + /** Location of the adb command line executable */ + private static String sAdbLocation; + private static String sToolsFolder; + private static String sHprofConverter; + + private boolean mHasDebuggerConnectors; + /** + * debugger connectors for already running apps. Initialized from an + * extension point. + */ + private IDebuggerConnector[] mDebuggerConnectors; + private ITraceviewLauncher[] mTraceviewLaunchers; + private List mClientSpecificActions = null; + + /** Console for DDMS log message */ + private MessageConsole mDdmsConsole; + + private IDevice mCurrentDevice; + private Client mCurrentClient; + private boolean mListeningToUiSelection = false; + + private final ArrayList mListeners = new ArrayList(); + + private Color mRed; + private ImageFactory mImageFactory; + + /** + * Classes which implement this interface provide methods that deals with + * {@link IDevice} and {@link Client} selectionchanges. + */ + public interface ISelectionListener { + + /** + * Sent when a new {@link Client} is selected. + * + * @param selectedClient + * The selected client. If null, no clients are selected. + */ + public void selectionChanged(Client selectedClient); + + /** + * Sent when a new {@link IDevice} is selected. + * + * @param selectedDevice + * the selected device. If null, no devices are selected. + */ + public void selectionChanged(IDevice selectedDevice); + } + + /** + * The constructor + */ + public DdmsPlugin() { + sPlugin = this; + } + + public ImageFactory getImageFactory() { + return mImageFactory; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext + * ) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + final Display display = getDisplay(); + + // get the eclipse store + final IPreferenceStore eclipseStore = getPreferenceStore(); + + AndroidDebugBridge.addDeviceChangeListener(this); + + DdmUiPreferences.setStore(eclipseStore); + + // DdmUiPreferences.displayCharts(); + + // set the consoles. + mDdmsConsole = new MessageConsole("DDMS", null); //$NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { mDdmsConsole }); + + final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); + final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non UI thread, and + // because + // changing the console properties update the UI, we need to make this + // change + // in the UI thread. + display.asyncExec(new Runnable() { + @Override + public void run() { + errorConsoleStream.setColor(mRed); + } + }); + + // set up the ddms log to use the ddms console. + Log.addLogger(new ILogOutput() { + @Override + public void printLog(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { + printToStream(errorConsoleStream, tag, message); + showConsoleView(mDdmsConsole); + } else { + printToStream(consoleStream, tag, message); + } + } + + @Override + public void printAndPromptLog(final LogLevel logLevel, final String tag, final String message) { + printLog(logLevel, tag, message); + // dialog box only run in UI thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + if (logLevel == LogLevel.ERROR) { + MessageDialog.openError(shell, tag, message); + } else { + MessageDialog.openWarning(shell, tag, message); + } + } + }); + } + + }); + + // set up the ddms console to use this objects + DdmConsole.setConsole(new IDdmConsole() { + @Override + public void printErrorToConsole(String message) { + printToStream(errorConsoleStream, null, message); + showConsoleView(mDdmsConsole); + } + + @Override + public void printErrorToConsole(String[] messages) { + for (String m : messages) { + printToStream(errorConsoleStream, null, m); + } + showConsoleView(mDdmsConsole); + } + + @Override + public void printToConsole(String message) { + printToStream(consoleStream, null, message); + } + + @Override + public void printToConsole(String[] messages) { + for (String m : messages) { + printToStream(consoleStream, null, m); + } + } + }); + + // set the listener for the preference change + eclipseStore.addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + // get the name of the property that changed. + String property = event.getProperty(); + + if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { + DdmPreferences.setDebugPortBase(eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); + } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { + DdmPreferences.setSelectedDebugPort(eclipseStore + .getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); + } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { + DdmUiPreferences.setThreadRefreshInterval(eclipseStore + .getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); + } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { + DdmPreferences.setLogLevel(eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); + } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) { + DdmPreferences.setTimeOut(eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT)); + } else if (PreferenceInitializer.ATTR_USE_ADBHOST.equals(property)) { + DdmPreferences.setUseAdbHost(eclipseStore.getBoolean(PreferenceInitializer.ATTR_USE_ADBHOST)); + } else if (PreferenceInitializer.ATTR_ADBHOST_VALUE.equals(property)) { + DdmPreferences.setAdbHostValue(eclipseStore.getString(PreferenceInitializer.ATTR_ADBHOST_VALUE)); + } + } + }); + + // do some last initializations + + mImageFactory = new JFaceImageLoader(new DdmResourceProvider()); + // set the preferences. + PreferenceInitializer.setupPreferences(); + + // this class is set as the main source revealer and will look at all + // the implementations + // of the extension point. see #reveal(String, String, int) + StackTracePanel.setSourceRevealer(this); + + /* + * Load the extension point implementations. The first step is to load + * the IConfigurationElement representing the implementations. The 2nd + * step is to use these objects to instantiate the implementation + * classes. + * + * Because the 2nd step will trigger loading the plug-ins providing the + * implementations, and those plug-ins could access DDMS classes (like + * ADT), this 2nd step should be done in a Job to ensure that DDMS is + * loaded, so that the other plug-ins can load. + * + * Both steps could be done in the 2nd step but some of DDMS UI rely on + * knowing if there is an implementation or not (DeviceView), so we do + * the first steps in start() and, in some case, record it. + */ + + // get the IConfigurationElement for the debuggerConnector right away. + final IConfigurationElement[] dcce = findConfigElements("org.eclipse.andmore.ddms.debuggerConnector"); //$NON-NLS-1$ + mHasDebuggerConnectors = dcce.length > 0; + + // get the other configElements and instantiante them in a Job. + new Job(Messages.DdmsPlugin_DDMS_Post_Create_Init) { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + // init the lib + AndroidDebugBridge.init(true /* debugger support */); + + // get the available adb locators + IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.toolsLocator"); //$NON-NLS-1$ + + IToolsLocator[] locators = instantiateToolsLocators(elements); + + for (IToolsLocator locator : locators) { + try { + String adbLocation = locator.getAdbLocation(); + String traceviewLocation = locator.getTraceViewLocation(); + String hprofConvLocation = locator.getHprofConvLocation(); + if (adbLocation != null && traceviewLocation != null && hprofConvLocation != null) { + // checks if the location is valid. + if (setToolsLocation(adbLocation, hprofConvLocation, traceviewLocation)) { + + AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); + + // no need to look at the other locators. + break; + } + } + } catch (Throwable t) { + // ignore, we'll just not use this implementation. + } + } + + // get the available debugger connectors + mDebuggerConnectors = instantiateDebuggerConnectors(dcce); + + // get the available Traceview Launchers. + elements = findConfigElements("org.eclipse.andmore.ddms.traceviewLauncher"); //$NON-NLS-1$ + mTraceviewLaunchers = instantiateTraceviewLauncher(elements); + + return Status.OK_STATUS; + } catch (CoreException e) { + return e.getStatus(); + } + } + }.schedule(); + } + + private void showConsoleView(MessageConsole console) { + ConsolePlugin.getDefault().getConsoleManager().showConsoleView(console); + } + + /** + * Obtain a list of configuration elements that extend the given extension + * point. + */ + IConfigurationElement[] findConfigElements(String extensionPointId) { + // get the adb location from an implementation of the ADB Locator + // extension point. + IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry(); + // NPE occurred during testing + if (extensionRegistry != null) + { + IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint(extensionPointId); + if (extensionPoint != null) { + return extensionPoint.getConfigurationElements(); + } + } + // shouldn't happen or it means the plug-in is broken. + return new IConfigurationElement[0]; + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called adbLocator. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private IToolsLocator[] instantiateToolsLocators(IConfigurationElement[] configElements) throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof IToolsLocator) { + list.add((IToolsLocator) obj); + } + } + + return list.toArray(new IToolsLocator[list.size()]); + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called debuggerConnector. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private IDebuggerConnector[] instantiateDebuggerConnectors(IConfigurationElement[] configElements) + throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof IDebuggerConnector) { + list.add((IDebuggerConnector) obj); + } + } + + return list.toArray(new IDebuggerConnector[list.size()]); + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called traceviewLauncher. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private ITraceviewLauncher[] instantiateTraceviewLauncher(IConfigurationElement[] configElements) + throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof ITraceviewLauncher) { + list.add((ITraceviewLauncher) obj); + } + } + + return list.toArray(new ITraceviewLauncher[list.size()]); + } + + /** + * Returns the classes that implement {@link IClientAction} in each of the + * extensions that extend clientAction extension point. + * + * @throws CoreException + */ + private List instantiateClientSpecificActions(IConfigurationElement[] elements) throws CoreException { + if (elements == null || elements.length == 0) { + return Collections.emptyList(); + } + + List extensions = new ArrayList(1); + + for (IConfigurationElement e : elements) { + Object o = e.createExecutableExtension("class"); //$NON-NLS-1$ + if (o instanceof IClientAction) { + extensions.add((IClientAction) o); + } + } + + return extensions; + } + + public static Display getDisplay() { + IWorkbench bench = sPlugin.getWorkbench(); + if (bench != null) { + return bench.getDisplay(); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext + * ) + */ + @Override + public void stop(BundleContext context) throws Exception { + AndroidDebugBridge.removeDeviceChangeListener(this); + + AndroidDebugBridge.terminate(); + + mRed.dispose(); + mImageFactory.dispose(); + sPlugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static DdmsPlugin getDefault() { + return sPlugin; + } + + public static String getAdb() { + return sAdbLocation; + } + + public static File getPlatformToolsFolder() { + return new File(sAdbLocation).getParentFile(); + } + + public static String getToolsFolder() { + return sToolsFolder; + } + + public static String getHprofConverter() { + return sHprofConverter; + } + + /** + * Stores the adb location. This returns true if the location is an existing + * file. + */ + private static boolean setToolsLocation(String adbLocation, String hprofConvLocation, String traceViewLocation) { + + File adb = new File(adbLocation); + File hprofConverter = new File(hprofConvLocation); + File traceview = new File(traceViewLocation); + + String missing = ""; + if (adb.isFile() == false) { + missing += adb.getAbsolutePath() + " "; + } + if (hprofConverter.isFile() == false) { + missing += hprofConverter.getAbsolutePath() + " "; + } + if (traceview.isFile() == false) { + missing += traceview.getAbsolutePath() + " "; + } + + if (missing.length() > 0) { + String msg = String.format("DDMS files not found: %1$s", missing); + Log.e("DDMS", msg); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, msg, null /* exception */); + getDefault().getLog().log(status); + return false; + } + + sAdbLocation = adbLocation; + sHprofConverter = hprofConverter.getAbsolutePath(); + DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath()); + + sToolsFolder = traceview.getParent(); + + return true; + } + + /** + * Set the location of the adb executable and optionally starts adb + * + * @param adb + * location of adb + * @param startAdb + * flag to start adb + */ + public static void setToolsLocation(String adbLocation, boolean startAdb, String hprofConvLocation, + String traceViewLocation) { + + if (setToolsLocation(adbLocation, hprofConvLocation, traceViewLocation)) { + // starts the server in a thread in case this is blocking. + if (startAdb) { + new Thread() { + @Override + public void run() { + // create and start the bridge + try { + AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */); + } catch (Throwable t) { + Status status = new Status(IStatus.ERROR, PLUGIN_ID, "Failed to create AndroidDebugBridge", + t); + getDefault().getLog().log(status); + } + } + }.start(); + } + } + } + + /** + * Returns whether there are implementations of the debuggerConnectors + * extension point. + *

+ * This is guaranteed to return the correct value as soon as the plug-in is + * loaded. + */ + public boolean hasDebuggerConnectors() { + return mHasDebuggerConnectors; + } + + /** + * Returns the implementations of {@link IDebuggerConnector}. + *

+ * There may be a small amount of time right after the plug-in load where + * this can return null even if there are implementation. + *

+ * Since the use of the implementation likely require user input, the UI can + * use {@link #hasDebuggerConnectors()} to know if there are implementations + * before they are loaded. + */ + public IDebuggerConnector[] getDebuggerConnectors() { + return mDebuggerConnectors; + } + + public synchronized void addSelectionListener(ISelectionListener listener) { + mListeners.add(listener); + + // notify the new listener of the current selection + listener.selectionChanged(mCurrentDevice); + listener.selectionChanged(mCurrentClient); + } + + public synchronized void removeSelectionListener(ISelectionListener listener) { + mListeners.remove(listener); + } + + public synchronized void setListeningState(boolean state) { + mListeningToUiSelection = state; + } + + /** + * Sent when the a device is connected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * + * @param device + * the new device. + * + * @see IDeviceChangeListener#deviceConnected(IDevice) + */ + @Override + public void deviceConnected(IDevice device) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + if (mCurrentDevice == null) { + handleDefaultSelection(device); + } + } + } + + /** + * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. + *

+ * This is sent from a non UI thread. + * + * @param device + * the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(IDevice) + */ + @Override + public void deviceDisconnected(IDevice device) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + // test if the disconnected device was the default selection. + if (mCurrentDevice == device) { + // try to find a new device + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + // get the device list + IDevice[] devices = bridge.getDevices(); + + // check if we still have devices + if (devices.length == 0) { + handleDefaultSelection((IDevice) null); + } else { + handleDefaultSelection(devices[0]); + } + } else { + handleDefaultSelection((IDevice) null); + } + } + } + } + + /** + * Sent when a device data changed, or when clients are started/terminated + * on the device. + *

+ * This is sent from a non UI thread. + * + * @param device + * the device that was updated. + * @param changeMask + * the mask indicating what changed. + * + * @see IDeviceChangeListener#deviceChanged(IDevice) + */ + @Override + public void deviceChanged(IDevice device, int changeMask) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + + // check if this is our device + if (device == mCurrentDevice) { + if (mCurrentClient == null) { + handleDefaultSelection(device); + } else { + // get the clients and make sure ours is still in there. + Client[] clients = device.getClients(); + boolean foundClient = false; + for (Client client : clients) { + if (client == mCurrentClient) { + foundClient = true; + break; + } + } + + // if we haven't found our client, lets look for a new one + if (foundClient == false) { + mCurrentClient = null; + handleDefaultSelection(device); + } + } + } + } + } + + /** + * 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. + */ + @Override + public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) { + if (mCurrentDevice != selectedDevice) { + mCurrentDevice = selectedDevice; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (mCurrentClient != selectedClient) { + mCurrentClient = selectedClient; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + } + + /** + * Handles a default selection of a {@link IDevice} and {@link Client}. + * + * @param device + * the selected device + */ + private void handleDefaultSelection(final IDevice device) { + // because the listener expect to receive this from the UI thread, and + // this is called + // from the AndroidDebugBridge notifications, we need to run this in the + // UI thread. + try { + Display display = getDisplay(); + + display.asyncExec(new Runnable() { + @Override + public void run() { + // set the new device if different. + boolean newDevice = false; + if (mCurrentDevice != device) { + mCurrentDevice = device; + newDevice = true; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (device != null) { + // if this is a device switch or the same device but we + // didn't find a valid + // client the last time, we go look for a client to use + // again. + if (newDevice || mCurrentClient == null) { + // now get the new client + Client[] clients = device.getClients(); + if (clients.length > 0) { + handleDefaultSelection(clients[0]); + } else { + handleDefaultSelection((Client) null); + } + } + } else { + handleDefaultSelection((Client) null); + } + } + }); + } catch (SWTException e) { + // display is disposed. Do nothing since we're quitting anyway. + } + } + + private void handleDefaultSelection(Client client) { + mCurrentClient = client; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + + /** + * Prints a message, associated with a project to the specified stream + * + * @param stream + * The stream to write to + * @param tag + * The tag associated to the message. Can be null + * @param message + * The message to print. + */ + private static synchronized void printToStream(MessageConsoleStream stream, String tag, String message) { + String dateTag = getMessageTag(tag); + + stream.print(dateTag); + if (!dateTag.endsWith(" ")) { + stream.print(" "); //$NON-NLS-1$ + } + stream.println(message); + } + + /** + * Creates a string containing the current date/time, and the tag + * + * @param tag + * The tag associated to the message. Can be null + * @return The dateTag + */ + private static String getMessageTag(String tag) { + Calendar c = Calendar.getInstance(); + + if (tag == null) { + return String.format(Messages.DdmsPlugin_Message_Tag_Mask_1, c); + } + + return String.format(Messages.DdmsPlugin_Message_Tag_Mask_2, c, tag); + } + + /** + * Implementation of com.android.ddmuilib.StackTracePanel.ISourceRevealer. + */ + @Override + public void reveal(String applicationName, String className, int line) { + JavaSourceRevealer.reveal(applicationName, className, line); + } + + public boolean launchTraceview(String osPath) { + if (mTraceviewLaunchers != null) { + for (ITraceviewLauncher launcher : mTraceviewLaunchers) { + try { + if (launcher.openFile(osPath)) { + return true; + } + } catch (Throwable t) { + // ignore, we'll just not use this implementation. + } + } + } + + return false; + } + + /** + * Returns the list of clients that extend the clientAction extension point. + */ + @NonNull + public synchronized List getClientSpecificActions() { + if (mClientSpecificActions == null) { + // get available client specific action extensions + IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.clientAction"); //$NON-NLS-1$ + try { + mClientSpecificActions = instantiateClientSpecificActions(elements); + } catch (CoreException e) { + mClientSpecificActions = Collections.emptyList(); + } + } + + return mClientSpecificActions; + } + + private LogCatMonitor mLogCatMonitor; + + public void startLogCatMonitor(IDevice device) { + if (mLogCatMonitor == null) { + mLogCatMonitor = new LogCatMonitor(getDebuggerConnectors(), getPreferenceStore()); + } + + mLogCatMonitor.monitorDevice(device); + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java index 3c5b1950..f865dd51 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java @@ -1,47 +1,51 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.AllocationPanel; - -import org.eclipse.swt.widgets.Composite; - -public class AllocTrackerView extends TableView { - - public static final String ID = "org.eclipse.andmore.ddms.views.AllocTrackerView"; //$NON-NLS-1$ - private AllocationPanel mPanel; - - public AllocTrackerView() { - } - - @Override - public void createPartControl(Composite parent) { - mPanel = new AllocationPanel(); - mPanel.createPanel(parent); - - setSelectionDependentPanel(mPanel); - - // listen to focus changes for table(s) of the panel. - setupTableFocusListener(mPanel, parent); - } - - @Override - public void setFocus() { - mPanel.setFocus(); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.AllocationPanel; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.swt.widgets.Composite; + +public class AllocTrackerView extends TableView { + + public static final String ID = "org.eclipse.andmore.ddms.views.AllocTrackerView"; //$NON-NLS-1$ + private AllocationPanel mPanel; + private ImageFactory mImageFactory; + + public AllocTrackerView() { + super(); + mImageFactory = DdmsPlugin.getDefault().getImageFactory(); + } + + @Override + public void createPartControl(Composite parent) { + mPanel = new AllocationPanel(mImageFactory); + mPanel.createPanel(parent); + + setSelectionDependentPanel(mPanel); + + // listen to focus changes for table(s) of the panel. + setupTableFocusListener(mPanel, parent); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java index a467a1a0..2aa62d79 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java @@ -1,839 +1,839 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -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.CollectingOutputReceiver; -import com.android.ddmlib.DdmPreferences; -import com.android.ddmlib.IDevice; -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.DevicePanel; -import com.android.ddmuilib.DevicePanel.IUiSelectionListener; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.ScreenShotDialog; -import com.android.ddmuilib.SyncProgressHelper; -import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; -import com.android.ddmuilib.handler.BaseFileHandler; -import com.android.ddmuilib.handler.MethodProfilingHandler; -import com.android.uiautomator.UiAutomatorHelper; -import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; -import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; -import com.google.common.io.Files; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.IClientAction; -import org.eclipse.andmore.ddms.IDebuggerConnector; -import org.eclipse.andmore.ddms.editors.UiAutomatorViewer; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.andmore.ddms.systrace.ISystraceOptions; -import org.eclipse.andmore.ddms.systrace.ISystraceOptionsDialog; -import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV1; -import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV2; -import org.eclipse.andmore.ddms.systrace.SystraceOutputParser; -import org.eclipse.andmore.ddms.systrace.SystraceTask; -import org.eclipse.andmore.ddms.systrace.SystraceVersionDetector; -import org.eclipse.core.filesystem.EFS; -import org.eclipse.core.filesystem.IFileStore; -import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Path; -import org.eclipse.core.runtime.Status; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.jface.dialogs.ErrorDialog; -import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.jface.dialogs.ProgressMonitorDialog; -import org.eclipse.jface.operation.IRunnableWithProgress; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.window.Window; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.PartInitException; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.WorkbenchException; -import org.eclipse.ui.ide.IDE; -import org.eclipse.ui.part.ViewPart; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public class DeviceView extends ViewPart implements IUiSelectionListener, IClientChangeListener { - - private final static boolean USE_SELECTED_DEBUG_PORT = true; - - public static final String ID = "org.eclipse.andmore.ddms.views.DeviceView"; //$NON-NLS-1$ - - private static DeviceView sThis; - - private Shell mParentShell; - private DevicePanel mDeviceList; - - private Action mResetAdbAction; - private Action mCaptureAction; - private Action mViewUiAutomatorHierarchyAction; - private Action mSystraceAction; - private Action mUpdateThreadAction; - private Action mUpdateHeapAction; - private Action mGcAction; - private Action mKillAppAction; - private Action mDebugAction; - private Action mHprofAction; - private Action mTracingAction; - - private ImageDescriptor mTracingStartImage; - private ImageDescriptor mTracingStopImage; - - public class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { - public final static String ACTION_SAVE = "hprof.save"; //$NON-NLS-1$ - public final static String ACTION_OPEN = "hprof.open"; //$NON-NLS-1$ - - public final static String DOT_HPROF = ".hprof"; //$NON-NLS-1$ - - HProfHandler(Shell parentShell) { - super(parentShell); - } - - @Override - protected String getDialogTitle() { - return Messages.DeviceView_HPROF_Error; - } - - @Override - public void onEndFailure(final Client client, final String message) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - try { - displayErrorFromUiThread(Messages.DeviceView_Unable_Create_HPROF_For_Application, client - .getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); //$NON-NLS-1$ //$NON-NLS-2$ - } 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 - doSelectionChanged(mDeviceList.getSelectedClient()); - } - } - }); - } - - @Override - public void onSuccess(final String remoteFilePath, final Client client) { - mParentShell.getDisplay().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) { - // get from the preference what action to take - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); - - if (ACTION_OPEN.equals(value)) { - File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ - final String tempPath = temp.getAbsolutePath(); - 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(Messages.DeviceView_Pulling_From_Device, remoteFilePath), mParentShell); - - open(tempPath); - } else { - // default action is ACTION_SAVE - promptAndPull(sync, client.getClientData().getClientDescription() + DOT_HPROF, - remoteFilePath, Messages.DeviceView_Save_HPROF_File); - - } - } else { - displayErrorFromUiThread( - Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_First_Message, - device.getSerialNumber()); - } - } catch (SyncException e) { - if (e.wasCanceled() == false) { - displayErrorFromUiThread(Messages.DeviceView_Unable_Download_HPROF_From_Device_Two_Param, - device.getSerialNumber(), e.getMessage()); - } - } catch (Exception e) { - displayErrorFromUiThread( - Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_Second_Message, - 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 - doSelectionChanged(mDeviceList.getSelectedClient()); - } - } - }); - } - - @Override - public void onSuccess(final byte[] data, final Client client) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - // get from the preference what action to take - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); - - if (ACTION_OPEN.equals(value)) { - try { - // no need to give an extension since we're going to - // convert the - // file anyway after. - File tempFile = saveTempFile(data, null /* extension */); - open(tempFile.getAbsolutePath()); - } catch (Exception e) { - String errorMsg = e.getMessage(); - displayErrorFromUiThread(Messages.DeviceView_Failed_To_Save_HPROF_Data, - errorMsg != null ? ":\n" + errorMsg : "."); //$NON-NLS-1$ //$NON-NLS-2$ - } - } else { - // default action is ACTION_SAVE - promptAndSave(client.getClientData().getClientDescription() + DOT_HPROF, data, - Messages.DeviceView_Save_HPROF_File); - } - } - }); - } - - private void open(String path) throws IOException, InterruptedException, PartInitException { - // make a temp file to convert the hprof into something - // readable by normal tools - File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ - String tempPath = temp.getAbsolutePath(); - - String[] command = new String[3]; - command[0] = DdmsPlugin.getHprofConverter(); - command[1] = path; - command[2] = tempPath; - - Process p = Runtime.getRuntime().exec(command); - p.waitFor(); - - IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(tempPath)); - if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { - // before we open the file in an editor window, we make sure the - // current - // workbench page has an editor area (typically the ddms - // perspective doesn't). - IWorkbench workbench = PlatformUI.getWorkbench(); - IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); - IWorkbenchPage page = window.getActivePage(); - if (page == null) { - return; - } - - if (page.isEditorAreaVisible() == false) { - IAdaptable input; - input = page.getInput(); - try { - workbench.showPerspective("org.eclipse.debug.ui.DebugPerspective", //$NON-NLS-1$ - window, input); - } catch (WorkbenchException e) { - } - } - - IDE.openEditorOnFileStore(page, fileStore); - } - } - } - - public DeviceView() { - // the view is declared with allowMultiple="false" so we - // can safely do this. - sThis = this; - } - - public static DeviceView getInstance() { - return sThis; - } - - @Override - public void createPartControl(Composite parent) { - mParentShell = parent.getShell(); - - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - mDeviceList = new DevicePanel(USE_SELECTED_DEBUG_PORT); - mDeviceList.createPanel(parent); - mDeviceList.addSelectionListener(this); - - DdmsPlugin plugin = DdmsPlugin.getDefault(); - mDeviceList.addSelectionListener(plugin); - plugin.setListeningState(true); - - mCaptureAction = new Action(Messages.DeviceView_Screen_Capture) { - @Override - public void run() { - ScreenShotDialog dlg = new ScreenShotDialog(DdmsPlugin.getDisplay().getActiveShell()); - dlg.open(mDeviceList.getSelectedDevice()); - } - }; - mCaptureAction.setToolTipText(Messages.DeviceView_Screen_Capture_Tooltip); - mCaptureAction.setImageDescriptor(loader.loadDescriptor("capture.png")); //$NON-NLS-1$ - - mViewUiAutomatorHierarchyAction = new Action("Dump View Hierarchy for UI Automator") { - @Override - public void run() { - takeUiAutomatorSnapshot(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); - } - }; - mViewUiAutomatorHierarchyAction.setToolTipText("Dump View Hierarchy for UI Automator"); - mViewUiAutomatorHierarchyAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/uiautomator.png")); //$NON-NLS-1$ - - mSystraceAction = new Action("Capture System Wide Trace") { - @Override - public void run() { - launchSystrace(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); - } - }; - mSystraceAction.setToolTipText("Capture system wide trace using Android systrace"); - mSystraceAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/systrace.png")); //$NON-NLS-1$ - mSystraceAction.setEnabled(true); - - mResetAdbAction = new Action(Messages.DeviceView_Reset_ADB) { - @Override - public void run() { - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - if (bridge.restart() == false) { - // get the current Display - final Display display = DdmsPlugin.getDisplay(); - - // dialog box only run in ui thread.. - display.asyncExec(new Runnable() { - @Override - public void run() { - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_ADB_Error, - Messages.DeviceView_ADB_Failed_Restart); - } - }); - } - } - } - }; - mResetAdbAction.setToolTipText(Messages.DeviceView_Reset_ADB_Host_Deamon); - mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages() - .getImageDescriptor(ISharedImages.IMG_OBJS_WARN_TSK)); - - mKillAppAction = new Action() { - @Override - public void run() { - mDeviceList.killSelectedClient(); - } - }; - - mKillAppAction.setText(Messages.DeviceView_Stop_Process); - mKillAppAction.setToolTipText(Messages.DeviceView_Stop_Process_Tooltip); - mKillAppAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HALT)); - - mGcAction = new Action() { - @Override - public void run() { - mDeviceList.forceGcOnSelectedClient(); - } - }; - - mGcAction.setText(Messages.DeviceView_Cause_GC); - mGcAction.setToolTipText(Messages.DeviceView_Cause_GC_Tooltip); - mGcAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_GC)); - - mHprofAction = new Action() { - @Override - public void run() { - mDeviceList.dumpHprof(); - doSelectionChanged(mDeviceList.getSelectedClient()); - } - }; - mHprofAction.setText(Messages.DeviceView_Dump_HPROF_File); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Tooltip); - mHprofAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HPROF)); - - mUpdateHeapAction = new Action(Messages.DeviceView_Update_Heap, IAction.AS_CHECK_BOX) { - @Override - public void run() { - boolean enable = mUpdateHeapAction.isChecked(); - mDeviceList.setEnabledHeapOnSelectedClient(enable); - } - }; - mUpdateHeapAction.setToolTipText(Messages.DeviceView_Update_Heap_Tooltip); - mUpdateHeapAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HEAP)); - - mUpdateThreadAction = new Action(Messages.DeviceView_Threads, IAction.AS_CHECK_BOX) { - @Override - public void run() { - boolean enable = mUpdateThreadAction.isChecked(); - mDeviceList.setEnabledThreadOnSelectedClient(enable); - } - }; - mUpdateThreadAction.setToolTipText(Messages.DeviceView_Threads_Tooltip); - mUpdateThreadAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_THREAD)); - - mTracingAction = new Action() { - @Override - public void run() { - mDeviceList.toggleMethodProfiling(); - } - }; - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingStartImage = loader.loadDescriptor(DevicePanel.ICON_TRACING_START); - mTracingStopImage = loader.loadDescriptor(DevicePanel.ICON_TRACING_STOP); - mTracingAction.setImageDescriptor(mTracingStartImage); - - mDebugAction = new Action(Messages.DeviceView_Debug_Process) { - @Override - public void run() { - if (DdmsPlugin.getDefault().hasDebuggerConnectors()) { - Client currentClient = mDeviceList.getSelectedClient(); - if (currentClient != null) { - ClientData clientData = currentClient.getClientData(); - - // make sure the client can be debugged - switch (clientData.getDebuggerConnectionStatus()) { - case ERROR: { - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - Messages.DeviceView_Process_Debug_Already_In_Use); - return; - } - case ATTACHED: { - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - Messages.DeviceView_Process_Already_Being_Debugged); - return; - } - } - - // get the name of the client - String packageName = clientData.getClientDescription(); - if (packageName != null) { - - // try all connectors till one returns true. - IDebuggerConnector[] connectors = DdmsPlugin.getDefault().getDebuggerConnectors(); - - if (connectors != null) { - for (IDebuggerConnector connector : connectors) { - try { - if (connector.connectDebugger(packageName, - currentClient.getDebuggerListenPort(), - DdmPreferences.getSelectedDebugPort())) { - return; - } - } catch (Throwable t) { - // ignore, we'll just not use this - // implementation - } - } - } - - // if we get to this point, then we failed to find a - // project - // that matched the application to debug - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - String.format(Messages.DeviceView_Debug_Session_Failed, packageName)); - } - } - } - } - }; - mDebugAction.setToolTipText(Messages.DeviceView_Debug_Process_Tooltip); - mDebugAction.setImageDescriptor(loader.loadDescriptor("debug-attach.png")); //$NON-NLS-1$ - mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); - - placeActions(); - - // disabling all action buttons - selectionChanged(null, null); - - ClientData.setHprofDumpHandler(new HProfHandler(mParentShell)); - AndroidDebugBridge.addClientChangeListener(this); - ClientData.setMethodProfilingHandler(new MethodProfilingHandler(mParentShell) { - @Override - protected void open(String tempPath) { - if (DdmsPlugin.getDefault().launchTraceview(tempPath) == false) { - super.open(tempPath); - } - } - }); - } - - private void takeUiAutomatorSnapshot(final IDevice device, final Shell shell) { - ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell); - try { - dialog.run(true, false, new IRunnableWithProgress() { - @Override - public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { - UiAutomatorResult result = null; - try { - result = UiAutomatorHelper.takeSnapshot(device, monitor); - } catch (UiAutomatorException e) { - throw new InvocationTargetException(e); - } finally { - monitor.done(); - } - - UiAutomatorViewer.openEditor(result); - } - }); - } catch (Exception e) { - Throwable t = e; - if (e instanceof InvocationTargetException) { - t = ((InvocationTargetException) e).getTargetException(); - } - Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, "Error obtaining UI hierarchy", t); - ErrorDialog.openError(shell, "UI Automator", "Unexpected error while obtaining UI hierarchy", s); - } - }; - - private void launchSystrace(final IDevice device, final Shell parentShell) { - final File systraceAssets = new File(DdmsPlugin.getPlatformToolsFolder(), "systrace"); //$NON-NLS-1$ - if (!systraceAssets.isDirectory()) { - MessageDialog.openError(parentShell, "Systrace", - "Updated version of platform-tools (18.0.1 or greater) is required.\n" - + "Please update your platform-tools using SDK Manager."); - return; - } - - SystraceVersionDetector detector = new SystraceVersionDetector(device); - try { - new ProgressMonitorDialog(parentShell).run(true, false, detector); - } catch (InvocationTargetException e) { - MessageDialog.openError(parentShell, "Systrace", "Unexpected error while detecting atrace version: " + e); - return; - } catch (InterruptedException e) { - return; - } - - final ISystraceOptionsDialog dlg; - if (detector.getVersion() == SystraceVersionDetector.SYSTRACE_V1) { - dlg = new SystraceOptionsDialogV1(parentShell); - } else { - Client[] clients = device.getClients(); - List apps = new ArrayList(clients.length); - for (int i = 0; i < clients.length; i++) { - String name = clients[i].getClientData().getClientDescription(); - if (name != null && !name.isEmpty()) { - apps.add(name); - } - } - dlg = new SystraceOptionsDialogV2(parentShell, detector.getTags(), apps); - } - - if (dlg.open() != Window.OK) { - return; - } - - final ISystraceOptions options = dlg.getSystraceOptions(); - - // set trace tag if necessary: - // adb shell setprop debug.atrace.tags.enableflags - String tag = options.getTags(); - if (tag != null) { - CountDownLatch setTagLatch = new CountDownLatch(1); - CollectingOutputReceiver receiver = new CollectingOutputReceiver(setTagLatch); - try { - String cmd = "setprop debug.atrace.tags.enableflags " + tag; - device.executeShellCommand(cmd, receiver); - setTagLatch.await(5, TimeUnit.SECONDS); - } catch (Exception e) { - MessageDialog.openError(parentShell, "Systrace", "Unexpected error while setting trace tags: " + e); - return; - } - - String shellOutput = receiver.getOutput(); - if (shellOutput.contains("Error type")) { //$NON-NLS-1$ - throw new RuntimeException(receiver.getOutput()); - } - } - - // obtain the output of "adb shell atrace " and generate - // the html file - ProgressMonitorDialog d = new ProgressMonitorDialog(parentShell); - try { - d.run(true, true, new IRunnableWithProgress() { - @Override - public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { - boolean COMPRESS_DATA = true; - - monitor.setTaskName("Collecting Trace Information"); - final String atraceOptions = options.getOptions() + (COMPRESS_DATA ? " -z" : ""); - SystraceTask task = new SystraceTask(device, atraceOptions); - Thread t = new Thread(task, "Systrace Output Receiver"); - t.start(); - - // check if the user has cancelled tracing every so often - while (true) { - t.join(1000); - - if (t.isAlive()) { - if (monitor.isCanceled()) { - task.cancel(); - return; - } - } else { - break; - } - } - - if (task.getError() != null) { - throw new RuntimeException(task.getError()); - } - - monitor.setTaskName("Saving trace information"); - SystraceOutputParser parser = new SystraceOutputParser(COMPRESS_DATA, SystraceOutputParser - .getJs(systraceAssets), SystraceOutputParser.getCss(systraceAssets), SystraceOutputParser - .getHtmlPrefix(systraceAssets), SystraceOutputParser.getHtmlSuffix(systraceAssets)); - - parser.parse(task.getAtraceOutput()); - - String html = parser.getSystraceHtml(); - try { - Files.write(html.getBytes(), new File(dlg.getTraceFilePath())); - } catch (IOException e) { - throw new InvocationTargetException(e); - } - } - }); - } catch (InvocationTargetException e) { - ErrorDialog.openError(parentShell, "Systrace", "Unable to collect system trace.", new Status(IStatus.ERROR, - DdmsPlugin.PLUGIN_ID, "Unexpected error while collecting system trace.", e.getCause())); - } catch (InterruptedException ignore) { - } - } - - @Override - public void setFocus() { - mDeviceList.setFocus(); - } - - /** - * 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. - */ - @Override - public void selectionChanged(IDevice selectedDevice, Client selectedClient) { - // update the buttons - doSelectionChanged(selectedClient); - doSelectionChanged(selectedDevice); - } - - private void doSelectionChanged(Client selectedClient) { - // update the buttons - if (selectedClient != null) { - if (USE_SELECTED_DEBUG_PORT) { - // set the client as the debug client - selectedClient.setAsSelectedClient(); - } - - mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); - mKillAppAction.setEnabled(true); - mGcAction.setEnabled(true); - - mUpdateHeapAction.setEnabled(true); - mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled()); - - mUpdateThreadAction.setEnabled(true); - mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled()); - - ClientData data = selectedClient.getClientData(); - - if (data.hasFeature(ClientData.FEATURE_HPROF)) { - mHprofAction.setEnabled(data.hasPendingHprofDump() == false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); - } else { - mHprofAction.setEnabled(false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Not_Supported_By_VM); - } - - if (data.hasFeature(ClientData.FEATURE_PROFILING)) { - mTracingAction.setEnabled(true); - if (data.getMethodProfilingStatus() == MethodProfilingStatus.TRACER_ON - || data.getMethodProfilingStatus() == MethodProfilingStatus.SAMPLER_ON) { - mTracingAction.setToolTipText(Messages.DeviceView_Stop_Method_Profiling_Tooltip); - mTracingAction.setText(Messages.DeviceView_Stop_Method_Profiling); - mTracingAction.setImageDescriptor(mTracingStopImage); - } else { - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - } else { - mTracingAction.setEnabled(false); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Not_Suported_By_Vm); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - } else { - if (USE_SELECTED_DEBUG_PORT) { - // set the client as the debug client - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - bridge.setSelectedClient(null); - } - } - - mDebugAction.setEnabled(false); - mKillAppAction.setEnabled(false); - mGcAction.setEnabled(false); - mUpdateHeapAction.setChecked(false); - mUpdateHeapAction.setEnabled(false); - mUpdateThreadAction.setEnabled(false); - mUpdateThreadAction.setChecked(false); - mHprofAction.setEnabled(false); - - mHprofAction.setEnabled(false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); - - mTracingAction.setEnabled(false); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - a.selectedClientChanged(selectedClient); - } - } - - private void doSelectionChanged(IDevice selectedDevice) { - boolean validDevice = selectedDevice != null; - - mCaptureAction.setEnabled(validDevice); - mViewUiAutomatorHierarchyAction.setEnabled(validDevice); - mSystraceAction.setEnabled(validDevice); - } - - /** - * Place the actions in the ui. - */ - private final void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.removeAll(); - menuManager.add(mDebugAction); - menuManager.add(new Separator()); - menuManager.add(mUpdateHeapAction); - menuManager.add(mHprofAction); - menuManager.add(mGcAction); - menuManager.add(new Separator()); - menuManager.add(mUpdateThreadAction); - menuManager.add(mTracingAction); - menuManager.add(new Separator()); - menuManager.add(mKillAppAction); - menuManager.add(new Separator()); - menuManager.add(mCaptureAction); - menuManager.add(new Separator()); - menuManager.add(mViewUiAutomatorHierarchyAction); - menuManager.add(new Separator()); - menuManager.add(mSystraceAction); - menuManager.add(new Separator()); - menuManager.add(mResetAdbAction); - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - menuManager.add(a.getAction()); - } - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - toolBarManager.removeAll(); - toolBarManager.add(mDebugAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mUpdateHeapAction); - toolBarManager.add(mHprofAction); - toolBarManager.add(mGcAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mUpdateThreadAction); - toolBarManager.add(mTracingAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mKillAppAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mCaptureAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mViewUiAutomatorHierarchyAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mSystraceAction); - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - toolBarManager.add(a.getAction()); - } - } - - @Override - public void clientChanged(final Client client, int changeMask) { - if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == Client.CHANGE_METHOD_PROFILING_STATUS) { - if (mDeviceList.getSelectedClient() == client) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - // force refresh of the button enabled state. - doSelectionChanged(client); - } - }); - } - } - } -} +/* + * 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 org.eclipse.andmore.ddms.views; + +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.CollectingOutputReceiver; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.IDevice; +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.DevicePanel; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.ScreenShotDialog; +import com.android.ddmuilib.SyncProgressHelper; +import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; +import com.android.ddmuilib.handler.BaseFileHandler; +import com.android.ddmuilib.handler.MethodProfilingHandler; +import com.android.uiautomator.UiAutomatorHelper; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; +import com.google.common.io.Files; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.IClientAction; +import org.eclipse.andmore.ddms.IDebuggerConnector; +import org.eclipse.andmore.ddms.editors.UiAutomatorViewer; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.andmore.ddms.systrace.ISystraceOptions; +import org.eclipse.andmore.ddms.systrace.ISystraceOptionsDialog; +import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV1; +import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV2; +import org.eclipse.andmore.ddms.systrace.SystraceOutputParser; +import org.eclipse.andmore.ddms.systrace.SystraceTask; +import org.eclipse.andmore.ddms.systrace.SystraceVersionDetector; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.WorkbenchException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.ViewPart; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class DeviceView extends ViewPart implements IUiSelectionListener, IClientChangeListener { + + private final static boolean USE_SELECTED_DEBUG_PORT = true; + + public static final String ID = "org.eclipse.andmore.ddms.views.DeviceView"; //$NON-NLS-1$ + + private static DeviceView sThis; + + private Shell mParentShell; + private DevicePanel mDeviceList; + + private Action mResetAdbAction; + private Action mCaptureAction; + private Action mViewUiAutomatorHierarchyAction; + private Action mSystraceAction; + private Action mUpdateThreadAction; + private Action mUpdateHeapAction; + private Action mGcAction; + private Action mKillAppAction; + private Action mDebugAction; + private Action mHprofAction; + private Action mTracingAction; + + private ImageDescriptor mTracingStartImage; + private ImageDescriptor mTracingStopImage; + + public class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { + public final static String ACTION_SAVE = "hprof.save"; //$NON-NLS-1$ + public final static String ACTION_OPEN = "hprof.open"; //$NON-NLS-1$ + + public final static String DOT_HPROF = ".hprof"; //$NON-NLS-1$ + + HProfHandler(Shell parentShell) { + super(parentShell); + } + + @Override + protected String getDialogTitle() { + return Messages.DeviceView_HPROF_Error; + } + + @Override + public void onEndFailure(final Client client, final String message) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + try { + displayErrorFromUiThread(Messages.DeviceView_Unable_Create_HPROF_For_Application, client + .getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); //$NON-NLS-1$ //$NON-NLS-2$ + } 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 + doSelectionChanged(mDeviceList.getSelectedClient()); + } + } + }); + } + + @Override + public void onSuccess(final String remoteFilePath, final Client client) { + mParentShell.getDisplay().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) { + // get from the preference what action to take + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); + + if (ACTION_OPEN.equals(value)) { + File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ + final String tempPath = temp.getAbsolutePath(); + 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(Messages.DeviceView_Pulling_From_Device, remoteFilePath), mParentShell); + + open(tempPath); + } else { + // default action is ACTION_SAVE + promptAndPull(sync, client.getClientData().getClientDescription() + DOT_HPROF, + remoteFilePath, Messages.DeviceView_Save_HPROF_File); + + } + } else { + displayErrorFromUiThread( + Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_First_Message, + device.getSerialNumber()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + displayErrorFromUiThread(Messages.DeviceView_Unable_Download_HPROF_From_Device_Two_Param, + device.getSerialNumber(), e.getMessage()); + } + } catch (Exception e) { + displayErrorFromUiThread( + Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_Second_Message, + 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 + doSelectionChanged(mDeviceList.getSelectedClient()); + } + } + }); + } + + @Override + public void onSuccess(final byte[] data, final Client client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + // get from the preference what action to take + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); + + if (ACTION_OPEN.equals(value)) { + try { + // no need to give an extension since we're going to + // convert the + // file anyway after. + File tempFile = saveTempFile(data, null /* extension */); + open(tempFile.getAbsolutePath()); + } catch (Exception e) { + String errorMsg = e.getMessage(); + displayErrorFromUiThread(Messages.DeviceView_Failed_To_Save_HPROF_Data, + errorMsg != null ? ":\n" + errorMsg : "."); //$NON-NLS-1$ //$NON-NLS-2$ + } + } else { + // default action is ACTION_SAVE + promptAndSave(client.getClientData().getClientDescription() + DOT_HPROF, data, + Messages.DeviceView_Save_HPROF_File); + } + } + }); + } + + private void open(String path) throws IOException, InterruptedException, PartInitException { + // make a temp file to convert the hprof into something + // readable by normal tools + File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ + String tempPath = temp.getAbsolutePath(); + + String[] command = new String[3]; + command[0] = DdmsPlugin.getHprofConverter(); + command[1] = path; + command[2] = tempPath; + + Process p = Runtime.getRuntime().exec(command); + p.waitFor(); + + IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(tempPath)); + if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { + // before we open the file in an editor window, we make sure the + // current + // workbench page has an editor area (typically the ddms + // perspective doesn't). + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + return; + } + + if (page.isEditorAreaVisible() == false) { + IAdaptable input; + input = page.getInput(); + try { + workbench.showPerspective("org.eclipse.debug.ui.DebugPerspective", //$NON-NLS-1$ + window, input); + } catch (WorkbenchException e) { + } + } + + IDE.openEditorOnFileStore(page, fileStore); + } + } + } + + public DeviceView() { + // the view is declared with allowMultiple="false" so we + // can safely do this. + sThis = this; + } + + public static DeviceView getInstance() { + return sThis; + } + + @Override + public void createPartControl(Composite parent) { + mParentShell = parent.getShell(); + + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + mDeviceList = new DevicePanel(USE_SELECTED_DEBUG_PORT, imageFactory); + mDeviceList.createPanel(parent); + mDeviceList.addSelectionListener(this); + + DdmsPlugin plugin = DdmsPlugin.getDefault(); + mDeviceList.addSelectionListener(plugin); + plugin.setListeningState(true); + + mCaptureAction = new Action(Messages.DeviceView_Screen_Capture) { + @Override + public void run() { + ScreenShotDialog dlg = new ScreenShotDialog(DdmsPlugin.getDisplay().getActiveShell()); + dlg.open(mDeviceList.getSelectedDevice()); + } + }; + mCaptureAction.setToolTipText(Messages.DeviceView_Screen_Capture_Tooltip); + mCaptureAction.setImageDescriptor(imageFactory.getDescriptorByName("capture.png")); //$NON-NLS-1$ + + mViewUiAutomatorHierarchyAction = new Action("Dump View Hierarchy for UI Automator") { + @Override + public void run() { + takeUiAutomatorSnapshot(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); + } + }; + mViewUiAutomatorHierarchyAction.setToolTipText("Dump View Hierarchy for UI Automator"); + mViewUiAutomatorHierarchyAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/uiautomator.png")); //$NON-NLS-1$ + + mSystraceAction = new Action("Capture System Wide Trace") { + @Override + public void run() { + launchSystrace(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); + } + }; + mSystraceAction.setToolTipText("Capture system wide trace using Android systrace"); + mSystraceAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/systrace.png")); //$NON-NLS-1$ + mSystraceAction.setEnabled(true); + + mResetAdbAction = new Action(Messages.DeviceView_Reset_ADB) { + @Override + public void run() { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + if (bridge.restart() == false) { + // get the current Display + final Display display = DdmsPlugin.getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_ADB_Error, + Messages.DeviceView_ADB_Failed_Restart); + } + }); + } + } + } + }; + mResetAdbAction.setToolTipText(Messages.DeviceView_Reset_ADB_Host_Deamon); + mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages() + .getImageDescriptor(ISharedImages.IMG_OBJS_WARN_TSK)); + + mKillAppAction = new Action() { + @Override + public void run() { + mDeviceList.killSelectedClient(); + } + }; + + mKillAppAction.setText(Messages.DeviceView_Stop_Process); + mKillAppAction.setToolTipText(Messages.DeviceView_Stop_Process_Tooltip); + mKillAppAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HALT)); + + mGcAction = new Action() { + @Override + public void run() { + mDeviceList.forceGcOnSelectedClient(); + } + }; + + mGcAction.setText(Messages.DeviceView_Cause_GC); + mGcAction.setToolTipText(Messages.DeviceView_Cause_GC_Tooltip); + mGcAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_GC)); + + mHprofAction = new Action() { + @Override + public void run() { + mDeviceList.dumpHprof(); + doSelectionChanged(mDeviceList.getSelectedClient()); + } + }; + mHprofAction.setText(Messages.DeviceView_Dump_HPROF_File); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Tooltip); + mHprofAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HPROF)); + + mUpdateHeapAction = new Action(Messages.DeviceView_Update_Heap, IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateHeapAction.isChecked(); + mDeviceList.setEnabledHeapOnSelectedClient(enable); + } + }; + mUpdateHeapAction.setToolTipText(Messages.DeviceView_Update_Heap_Tooltip); + mUpdateHeapAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HEAP)); + + mUpdateThreadAction = new Action(Messages.DeviceView_Threads, IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateThreadAction.isChecked(); + mDeviceList.setEnabledThreadOnSelectedClient(enable); + } + }; + mUpdateThreadAction.setToolTipText(Messages.DeviceView_Threads_Tooltip); + mUpdateThreadAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_THREAD)); + + mTracingAction = new Action() { + @Override + public void run() { + mDeviceList.toggleMethodProfiling(); + } + }; + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingStartImage = imageFactory.getDescriptorByName(DevicePanel.ICON_TRACING_START); + mTracingStopImage = imageFactory.getDescriptorByName(DevicePanel.ICON_TRACING_STOP); + mTracingAction.setImageDescriptor(mTracingStartImage); + + mDebugAction = new Action(Messages.DeviceView_Debug_Process) { + @Override + public void run() { + if (DdmsPlugin.getDefault().hasDebuggerConnectors()) { + Client currentClient = mDeviceList.getSelectedClient(); + if (currentClient != null) { + ClientData clientData = currentClient.getClientData(); + + // make sure the client can be debugged + switch (clientData.getDebuggerConnectionStatus()) { + case ERROR: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + Messages.DeviceView_Process_Debug_Already_In_Use); + return; + } + case ATTACHED: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + Messages.DeviceView_Process_Already_Being_Debugged); + return; + } + } + + // get the name of the client + String packageName = clientData.getClientDescription(); + if (packageName != null) { + + // try all connectors till one returns true. + IDebuggerConnector[] connectors = DdmsPlugin.getDefault().getDebuggerConnectors(); + + if (connectors != null) { + for (IDebuggerConnector connector : connectors) { + try { + if (connector.connectDebugger(packageName, + currentClient.getDebuggerListenPort(), + DdmPreferences.getSelectedDebugPort())) { + return; + } + } catch (Throwable t) { + // ignore, we'll just not use this + // implementation + } + } + } + + // if we get to this point, then we failed to find a + // project + // that matched the application to debug + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + String.format(Messages.DeviceView_Debug_Session_Failed, packageName)); + } + } + } + } + }; + mDebugAction.setToolTipText(Messages.DeviceView_Debug_Process_Tooltip); + mDebugAction.setImageDescriptor(imageFactory.getDescriptorByName("debug-attach.png")); //$NON-NLS-1$ + mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); + + placeActions(); + + // disabling all action buttons + selectionChanged(null, null); + + ClientData.setHprofDumpHandler(new HProfHandler(mParentShell)); + AndroidDebugBridge.addClientChangeListener(this); + ClientData.setMethodProfilingHandler(new MethodProfilingHandler(mParentShell) { + @Override + protected void open(String tempPath) { + if (DdmsPlugin.getDefault().launchTraceview(tempPath) == false) { + super.open(tempPath); + } + } + }); + } + + private void takeUiAutomatorSnapshot(final IDevice device, final Shell shell) { + ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell); + try { + dialog.run(true, false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + UiAutomatorResult result = null; + try { + result = UiAutomatorHelper.takeSnapshot(device, monitor); + } catch (UiAutomatorException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + + UiAutomatorViewer.openEditor(result); + } + }); + } catch (Exception e) { + Throwable t = e; + if (e instanceof InvocationTargetException) { + t = ((InvocationTargetException) e).getTargetException(); + } + Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, "Error obtaining UI hierarchy", t); + ErrorDialog.openError(shell, "UI Automator", "Unexpected error while obtaining UI hierarchy", s); + } + }; + + private void launchSystrace(final IDevice device, final Shell parentShell) { + final File systraceAssets = new File(DdmsPlugin.getPlatformToolsFolder(), "systrace"); //$NON-NLS-1$ + if (!systraceAssets.isDirectory()) { + MessageDialog.openError(parentShell, "Systrace", + "Updated version of platform-tools (18.0.1 or greater) is required.\n" + + "Please update your platform-tools using SDK Manager."); + return; + } + + SystraceVersionDetector detector = new SystraceVersionDetector(device); + try { + new ProgressMonitorDialog(parentShell).run(true, false, detector); + } catch (InvocationTargetException e) { + MessageDialog.openError(parentShell, "Systrace", "Unexpected error while detecting atrace version: " + e); + return; + } catch (InterruptedException e) { + return; + } + + final ISystraceOptionsDialog dlg; + if (detector.getVersion() == SystraceVersionDetector.SYSTRACE_V1) { + dlg = new SystraceOptionsDialogV1(parentShell); + } else { + Client[] clients = device.getClients(); + List apps = new ArrayList(clients.length); + for (int i = 0; i < clients.length; i++) { + String name = clients[i].getClientData().getClientDescription(); + if (name != null && !name.isEmpty()) { + apps.add(name); + } + } + dlg = new SystraceOptionsDialogV2(parentShell, detector.getTags(), apps); + } + + if (dlg.open() != Window.OK) { + return; + } + + final ISystraceOptions options = dlg.getSystraceOptions(); + + // set trace tag if necessary: + // adb shell setprop debug.atrace.tags.enableflags + String tag = options.getTags(); + if (tag != null) { + CountDownLatch setTagLatch = new CountDownLatch(1); + CollectingOutputReceiver receiver = new CollectingOutputReceiver(setTagLatch); + try { + String cmd = "setprop debug.atrace.tags.enableflags " + tag; + device.executeShellCommand(cmd, receiver); + setTagLatch.await(5, TimeUnit.SECONDS); + } catch (Exception e) { + MessageDialog.openError(parentShell, "Systrace", "Unexpected error while setting trace tags: " + e); + return; + } + + String shellOutput = receiver.getOutput(); + if (shellOutput.contains("Error type")) { //$NON-NLS-1$ + throw new RuntimeException(receiver.getOutput()); + } + } + + // obtain the output of "adb shell atrace " and generate + // the html file + ProgressMonitorDialog d = new ProgressMonitorDialog(parentShell); + try { + d.run(true, true, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + boolean COMPRESS_DATA = true; + + monitor.setTaskName("Collecting Trace Information"); + final String atraceOptions = options.getOptions() + (COMPRESS_DATA ? " -z" : ""); + SystraceTask task = new SystraceTask(device, atraceOptions); + Thread t = new Thread(task, "Systrace Output Receiver"); + t.start(); + + // check if the user has cancelled tracing every so often + while (true) { + t.join(1000); + + if (t.isAlive()) { + if (monitor.isCanceled()) { + task.cancel(); + return; + } + } else { + break; + } + } + + if (task.getError() != null) { + throw new RuntimeException(task.getError()); + } + + monitor.setTaskName("Saving trace information"); + SystraceOutputParser parser = new SystraceOutputParser(COMPRESS_DATA, SystraceOutputParser + .getJs(systraceAssets), SystraceOutputParser.getCss(systraceAssets), SystraceOutputParser + .getHtmlPrefix(systraceAssets), SystraceOutputParser.getHtmlSuffix(systraceAssets)); + + parser.parse(task.getAtraceOutput()); + + String html = parser.getSystraceHtml(); + try { + Files.write(html.getBytes(), new File(dlg.getTraceFilePath())); + } catch (IOException e) { + throw new InvocationTargetException(e); + } + } + }); + } catch (InvocationTargetException e) { + ErrorDialog.openError(parentShell, "Systrace", "Unable to collect system trace.", new Status(IStatus.ERROR, + DdmsPlugin.PLUGIN_ID, "Unexpected error while collecting system trace.", e.getCause())); + } catch (InterruptedException ignore) { + } + } + + @Override + public void setFocus() { + mDeviceList.setFocus(); + } + + /** + * 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. + */ + @Override + public void selectionChanged(IDevice selectedDevice, Client selectedClient) { + // update the buttons + doSelectionChanged(selectedClient); + doSelectionChanged(selectedDevice); + } + + private void doSelectionChanged(Client selectedClient) { + // update the buttons + if (selectedClient != null) { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + selectedClient.setAsSelectedClient(); + } + + mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); + mKillAppAction.setEnabled(true); + mGcAction.setEnabled(true); + + mUpdateHeapAction.setEnabled(true); + mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled()); + + mUpdateThreadAction.setEnabled(true); + mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled()); + + ClientData data = selectedClient.getClientData(); + + if (data.hasFeature(ClientData.FEATURE_HPROF)) { + mHprofAction.setEnabled(data.hasPendingHprofDump() == false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); + } else { + mHprofAction.setEnabled(false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Not_Supported_By_VM); + } + + if (data.hasFeature(ClientData.FEATURE_PROFILING)) { + mTracingAction.setEnabled(true); + if (data.getMethodProfilingStatus() == MethodProfilingStatus.TRACER_ON + || data.getMethodProfilingStatus() == MethodProfilingStatus.SAMPLER_ON) { + mTracingAction.setToolTipText(Messages.DeviceView_Stop_Method_Profiling_Tooltip); + mTracingAction.setText(Messages.DeviceView_Stop_Method_Profiling); + mTracingAction.setImageDescriptor(mTracingStopImage); + } else { + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + } else { + mTracingAction.setEnabled(false); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Not_Suported_By_Vm); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + } else { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + bridge.setSelectedClient(null); + } + } + + mDebugAction.setEnabled(false); + mKillAppAction.setEnabled(false); + mGcAction.setEnabled(false); + mUpdateHeapAction.setChecked(false); + mUpdateHeapAction.setEnabled(false); + mUpdateThreadAction.setEnabled(false); + mUpdateThreadAction.setChecked(false); + mHprofAction.setEnabled(false); + + mHprofAction.setEnabled(false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); + + mTracingAction.setEnabled(false); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + a.selectedClientChanged(selectedClient); + } + } + + private void doSelectionChanged(IDevice selectedDevice) { + boolean validDevice = selectedDevice != null; + + mCaptureAction.setEnabled(validDevice); + mViewUiAutomatorHierarchyAction.setEnabled(validDevice); + mSystraceAction.setEnabled(validDevice); + } + + /** + * Place the actions in the ui. + */ + private final void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.removeAll(); + menuManager.add(mDebugAction); + menuManager.add(new Separator()); + menuManager.add(mUpdateHeapAction); + menuManager.add(mHprofAction); + menuManager.add(mGcAction); + menuManager.add(new Separator()); + menuManager.add(mUpdateThreadAction); + menuManager.add(mTracingAction); + menuManager.add(new Separator()); + menuManager.add(mKillAppAction); + menuManager.add(new Separator()); + menuManager.add(mCaptureAction); + menuManager.add(new Separator()); + menuManager.add(mViewUiAutomatorHierarchyAction); + menuManager.add(new Separator()); + menuManager.add(mSystraceAction); + menuManager.add(new Separator()); + menuManager.add(mResetAdbAction); + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + menuManager.add(a.getAction()); + } + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.removeAll(); + toolBarManager.add(mDebugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpdateHeapAction); + toolBarManager.add(mHprofAction); + toolBarManager.add(mGcAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpdateThreadAction); + toolBarManager.add(mTracingAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mKillAppAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mCaptureAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mViewUiAutomatorHierarchyAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mSystraceAction); + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + toolBarManager.add(a.getAction()); + } + } + + @Override + public void clientChanged(final Client client, int changeMask) { + if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == Client.CHANGE_METHOD_PROFILING_STATUS) { + if (mDeviceList.getSelectedClient() == client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + // force refresh of the button enabled state. + doSelectionChanged(client); + } + }); + } + } + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java index 73d72625..098d491a 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java @@ -1,41 +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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.EmulatorControlPanel; - -import org.eclipse.swt.widgets.Composite; - -public class EmulatorControlView extends SelectionDependentViewPart { - - public static final String ID = "org.eclipse.andmore.ddms.views.EmulatorControlView"; //$NON-NLS-1$ - - private EmulatorControlPanel mPanel; - - @Override - public void createPartControl(Composite parent) { - mPanel = new EmulatorControlPanel(); - mPanel.createPanel(parent); - setSelectionDependentPanel(mPanel); - } - - @Override - public void setFocus() { - mPanel.setFocus(); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.EmulatorControlPanel; + +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.swt.widgets.Composite; + +public class EmulatorControlView extends SelectionDependentViewPart { + + public static final String ID = "org.eclipse.andmore.ddms.views.EmulatorControlView"; //$NON-NLS-1$ + + private EmulatorControlPanel mPanel; + + @Override + public void createPartControl(Composite parent) { + mPanel = new EmulatorControlPanel(DdmsPlugin.getDefault().getImageFactory()); + mPanel.createPanel(parent); + setSelectionDependentPanel(mPanel); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java index f7260b11..c5166602 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java @@ -1,110 +1,111 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.log.event.EventLogPanel; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; - -public class EventLogView extends SelectionDependentViewPart { - - private EventLogPanel mLogPanel; - - @Override - public void createPartControl(Composite parent) { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - // create the external actions - CommonAction optionsAction = new CommonAction(Messages.EventLogView_Options); - optionsAction.setToolTipText(Messages.EventLogView_Opens_Options_Panel); - optionsAction.setImageDescriptor(loader.loadDescriptor("edit.png")); //$NON-NLS-1$ - - CommonAction clearLogAction = new CommonAction(Messages.EventLogView_Clear_Log); - clearLogAction.setToolTipText(Messages.EventLogView_Clears_Event_Log); - clearLogAction.setImageDescriptor(loader.loadDescriptor("clear.png")); //$NON-NLS-1$ - - CommonAction saveAction = new CommonAction(Messages.EventLogView_Save_Log); - saveAction.setToolTipText(Messages.EventLogView_Saves_Event_Log); - saveAction.setImageDescriptor(loader.loadDescriptor("save.png")); //$NON-NLS-1$ - - CommonAction loadAction = new CommonAction(Messages.EventLogView_Load_Log); - loadAction.setToolTipText(Messages.EventLogView_Loads_Event_Log); - loadAction.setImageDescriptor(loader.loadDescriptor("load.png")); //$NON-NLS-1$ - - CommonAction importBugAction = new CommonAction(Messages.EventLogView_Import_Bug_Report_Log); - importBugAction.setToolTipText(Messages.EventLogView_Imports_Bug_Report); - importBugAction.setImageDescriptor(loader.loadDescriptor("importBug.png")); //$NON-NLS-1$ - - placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); - - mLogPanel = new EventLogPanel(); - mLogPanel.setActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); - mLogPanel.createPanel(parent); - setSelectionDependentPanel(mLogPanel); - } - - @Override - public void setFocus() { - mLogPanel.setFocus(); - } - - @Override - public void dispose() { - if (mLogPanel != null) { - mLogPanel.stopEventLog(true); - } - } - - /** - * Places the actions in the toolbar and in the menu. - * - * @param importBugAction - */ - private void placeActions(IAction optionAction, IAction clearAction, IAction saveAction, IAction loadAction, - CommonAction importBugAction) { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.add(clearAction); - menuManager.add(new Separator()); - menuManager.add(saveAction); - menuManager.add(loadAction); - menuManager.add(importBugAction); - menuManager.add(new Separator()); - menuManager.add(optionAction); - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - toolBarManager.add(clearAction); - toolBarManager.add(new Separator()); - toolBarManager.add(saveAction); - toolBarManager.add(loadAction); - toolBarManager.add(importBugAction); - toolBarManager.add(new Separator()); - toolBarManager.add(optionAction); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.log.event.EventLogPanel; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; + +public class EventLogView extends SelectionDependentViewPart { + + private EventLogPanel mLogPanel; + + @Override + public void createPartControl(Composite parent) { + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + // create the external actions + CommonAction optionsAction = new CommonAction(Messages.EventLogView_Options); + optionsAction.setToolTipText(Messages.EventLogView_Opens_Options_Panel); + optionsAction.setImageDescriptor(imageFactory.getDescriptorByName("edit.png")); //$NON-NLS-1$ + + CommonAction clearLogAction = new CommonAction(Messages.EventLogView_Clear_Log); + clearLogAction.setToolTipText(Messages.EventLogView_Clears_Event_Log); + clearLogAction.setImageDescriptor(imageFactory.getDescriptorByName("clear.png")); //$NON-NLS-1$ + + CommonAction saveAction = new CommonAction(Messages.EventLogView_Save_Log); + saveAction.setToolTipText(Messages.EventLogView_Saves_Event_Log); + saveAction.setImageDescriptor(imageFactory.getDescriptorByName("save.png")); //$NON-NLS-1$ + + CommonAction loadAction = new CommonAction(Messages.EventLogView_Load_Log); + loadAction.setToolTipText(Messages.EventLogView_Loads_Event_Log); + loadAction.setImageDescriptor(imageFactory.getDescriptorByName("load.png")); //$NON-NLS-1$ + + CommonAction importBugAction = new CommonAction(Messages.EventLogView_Import_Bug_Report_Log); + importBugAction.setToolTipText(Messages.EventLogView_Imports_Bug_Report); + importBugAction.setImageDescriptor(imageFactory.getDescriptorByName("importBug.png")); //$NON-NLS-1$ + + placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + + mLogPanel = new EventLogPanel(imageFactory); + mLogPanel.setActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + @Override + public void dispose() { + if (mLogPanel != null) { + mLogPanel.stopEventLog(true); + } + } + + /** + * Places the actions in the toolbar and in the menu. + * + * @param importBugAction + */ + private void placeActions(IAction optionAction, IAction clearAction, IAction saveAction, IAction loadAction, + CommonAction importBugAction) { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(clearAction); + menuManager.add(new Separator()); + menuManager.add(saveAction); + menuManager.add(loadAction); + menuManager.add(importBugAction); + menuManager.add(new Separator()); + menuManager.add(optionAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.add(clearAction); + toolBarManager.add(new Separator()); + toolBarManager.add(saveAction); + toolBarManager.add(loadAction); + toolBarManager.add(importBugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(optionAction); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java index 93cb09a3..afeb8884 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java @@ -1,177 +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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.Client; -import com.android.ddmlib.IDevice; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.explorer.DeviceExplorer; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.DdmsPlugin.ISelectionListener; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.swt.graphics.Device; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.part.ViewPart; - -public class FileExplorerView extends ViewPart implements ISelectionListener { - - public static final String ID = "org.eclipse.andmore.ddms.views.FileExplorerView"; //$NON-NLS-1$ - - private final static String COLUMN_NAME = DdmsPlugin.PLUGIN_ID + ".explorer.name"; //$NON-NLS-1S - private final static String COLUMN_SIZE = DdmsPlugin.PLUGIN_ID + ".explorer.size"; //$NON-NLS-1S - private final static String COLUMN_DATE = DdmsPlugin.PLUGIN_ID + ".explorer.data"; //$NON-NLS-1S - private final static String COLUMN_TIME = DdmsPlugin.PLUGIN_ID + ".explorer.time"; //$NON-NLS-1S - private final static String COLUMN_PERMISSIONS = DdmsPlugin.PLUGIN_ID + ".explorer.permissions"; //$NON-NLS-1S - private final static String COLUMN_INFO = DdmsPlugin.PLUGIN_ID + ".explorer.info"; //$NON-NLS-1$ - - private DeviceExplorer mExplorer; - - public FileExplorerView() { - } - - @Override - public void createPartControl(Composite parent) { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - DeviceExplorer.COLUMN_NAME = COLUMN_NAME; - DeviceExplorer.COLUMN_SIZE = COLUMN_SIZE; - DeviceExplorer.COLUMN_DATE = COLUMN_DATE; - DeviceExplorer.COLUMN_TIME = COLUMN_TIME; - DeviceExplorer.COLUMN_PERMISSIONS = COLUMN_PERMISSIONS; - DeviceExplorer.COLUMN_INFO = COLUMN_INFO; - - // device explorer - mExplorer = new DeviceExplorer(); - - mExplorer.setCustomImages(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE), - PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER), null /* - * apk - * image - */, PlatformUI - .getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ELEMENT)); - - // creates the actions - CommonAction pushAction = new CommonAction(Messages.FileExplorerView_Push_File) { - @Override - public void run() { - mExplorer.pushIntoSelection(); - } - }; - pushAction.setToolTipText(Messages.FileExplorerView_Push_File_Onto_Device); - pushAction.setImageDescriptor(loader.loadDescriptor("push.png")); //$NON-NLS-1$ - pushAction.setEnabled(false); - - CommonAction pullAction = new CommonAction(Messages.FileExplorerView_Pull_File) { - @Override - public void run() { - mExplorer.pullSelection(); - } - }; - pullAction.setToolTipText(Messages.FileExplorerView_Pull_File_From_File); - pullAction.setImageDescriptor(loader.loadDescriptor("pull.png")); //$NON-NLS-1$ - pullAction.setEnabled(false); - - CommonAction deleteAction = new CommonAction(Messages.FileExplorerView_Delete) { - @Override - public void run() { - mExplorer.deleteSelection(); - } - }; - deleteAction.setToolTipText(Messages.FileExplorerView_Delete_The_Selection); - deleteAction.setImageDescriptor(loader.loadDescriptor("delete.png")); //$NON-NLS-1$ - deleteAction.setEnabled(false); - - CommonAction createNewFolderAction = new CommonAction("New Folder") { - @Override - public void run() { - mExplorer.createNewFolderInSelection(); - } - }; - createNewFolderAction.setToolTipText("New Folder"); - createNewFolderAction.setImageDescriptor(loader.loadDescriptor("add.png")); //$NON-NLS-1$ - createNewFolderAction.setEnabled(false); - - // set up the actions in the explorer - mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction); - - // and in the ui - IActionBars actionBars = getViewSite().getActionBars(); - IMenuManager menuManager = actionBars.getMenuManager(); - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - - menuManager.add(pullAction); - menuManager.add(pushAction); - menuManager.add(new Separator()); - menuManager.add(deleteAction); - menuManager.add(new Separator()); - menuManager.add(createNewFolderAction); - - toolBarManager.add(pullAction); - toolBarManager.add(pushAction); - toolBarManager.add(new Separator()); - toolBarManager.add(deleteAction); - toolBarManager.add(new Separator()); - toolBarManager.add(createNewFolderAction); - - mExplorer.createPanel(parent); - - DdmsPlugin.getDefault().addSelectionListener(this); - } - - @Override - public void setFocus() { - mExplorer.setFocus(); - } - - /** - * Sent when a new {@link Client} is selected. - * - * @param selectedClient - * The selected client. - */ - @Override - public void selectionChanged(Client selectedClient) { - // pass - } - - /** - * Sent when a new {@link Device} is selected. - * - * @param selectedDevice - * the selected device. - */ - @Override - public void selectionChanged(IDevice selectedDevice) { - mExplorer.switchDevice(selectedDevice); - } - - /** - * Sent when there is no current selection. - */ - public void selectionRemoved() { - - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmuilib.explorer.DeviceExplorer; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.DdmsPlugin.ISelectionListener; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; + +public class FileExplorerView extends ViewPart implements ISelectionListener { + + public static final String ID = "org.eclipse.andmore.ddms.views.FileExplorerView"; //$NON-NLS-1$ + + private final static String COLUMN_NAME = DdmsPlugin.PLUGIN_ID + ".explorer.name"; //$NON-NLS-1S + private final static String COLUMN_SIZE = DdmsPlugin.PLUGIN_ID + ".explorer.size"; //$NON-NLS-1S + private final static String COLUMN_DATE = DdmsPlugin.PLUGIN_ID + ".explorer.data"; //$NON-NLS-1S + private final static String COLUMN_TIME = DdmsPlugin.PLUGIN_ID + ".explorer.time"; //$NON-NLS-1S + private final static String COLUMN_PERMISSIONS = DdmsPlugin.PLUGIN_ID + ".explorer.permissions"; //$NON-NLS-1S + private final static String COLUMN_INFO = DdmsPlugin.PLUGIN_ID + ".explorer.info"; //$NON-NLS-1$ + + private DeviceExplorer mExplorer; + + public FileExplorerView() { + } + + @Override + public void createPartControl(Composite parent) { + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + DeviceExplorer.COLUMN_NAME = COLUMN_NAME; + DeviceExplorer.COLUMN_SIZE = COLUMN_SIZE; + DeviceExplorer.COLUMN_DATE = COLUMN_DATE; + DeviceExplorer.COLUMN_TIME = COLUMN_TIME; + DeviceExplorer.COLUMN_PERMISSIONS = COLUMN_PERMISSIONS; + DeviceExplorer.COLUMN_INFO = COLUMN_INFO; + + // device explorer + mExplorer = new DeviceExplorer(imageFactory); + + mExplorer.setCustomImages(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE), + PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER), null /* + * apk + * image + */, PlatformUI + .getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ELEMENT)); + + // creates the actions + CommonAction pushAction = new CommonAction(Messages.FileExplorerView_Push_File) { + @Override + public void run() { + mExplorer.pushIntoSelection(); + } + }; + pushAction.setToolTipText(Messages.FileExplorerView_Push_File_Onto_Device); + pushAction.setImageDescriptor(imageFactory.getDescriptorByName("push.png")); //$NON-NLS-1$ + pushAction.setEnabled(false); + + CommonAction pullAction = new CommonAction(Messages.FileExplorerView_Pull_File) { + @Override + public void run() { + mExplorer.pullSelection(); + } + }; + pullAction.setToolTipText(Messages.FileExplorerView_Pull_File_From_File); + pullAction.setImageDescriptor(imageFactory.getDescriptorByName("pull.png")); //$NON-NLS-1$ + pullAction.setEnabled(false); + + CommonAction deleteAction = new CommonAction(Messages.FileExplorerView_Delete) { + @Override + public void run() { + mExplorer.deleteSelection(); + } + }; + deleteAction.setToolTipText(Messages.FileExplorerView_Delete_The_Selection); + deleteAction.setImageDescriptor(imageFactory.getDescriptorByName("delete.png")); //$NON-NLS-1$ + deleteAction.setEnabled(false); + + CommonAction createNewFolderAction = new CommonAction("New Folder") { + @Override + public void run() { + mExplorer.createNewFolderInSelection(); + } + }; + createNewFolderAction.setToolTipText("New Folder"); + createNewFolderAction.setImageDescriptor(imageFactory.getDescriptorByName("add.png")); //$NON-NLS-1$ + createNewFolderAction.setEnabled(false); + + // set up the actions in the explorer + mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction); + + // and in the ui + IActionBars actionBars = getViewSite().getActionBars(); + IMenuManager menuManager = actionBars.getMenuManager(); + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + + menuManager.add(pullAction); + menuManager.add(pushAction); + menuManager.add(new Separator()); + menuManager.add(deleteAction); + menuManager.add(new Separator()); + menuManager.add(createNewFolderAction); + + toolBarManager.add(pullAction); + toolBarManager.add(pushAction); + toolBarManager.add(new Separator()); + toolBarManager.add(deleteAction); + toolBarManager.add(new Separator()); + toolBarManager.add(createNewFolderAction); + + mExplorer.createPanel(parent); + + DdmsPlugin.getDefault().addSelectionListener(this); + } + + @Override + public void setFocus() { + mExplorer.setFocus(); + } + + /** + * Sent when a new {@link Client} is selected. + * + * @param selectedClient + * The selected client. + */ + @Override + public void selectionChanged(Client selectedClient) { + // pass + } + + /** + * Sent when a new {@link Device} is selected. + * + * @param selectedDevice + * the selected device. + */ + @Override + public void selectionChanged(IDevice selectedDevice) { + mExplorer.switchDevice(selectedDevice); + } + + /** + * Sent when there is no current selection. + */ + public void selectionRemoved() { + + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java index 78573463..9c374237 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java @@ -1,115 +1,115 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.logcat.LogCatMessage; -import com.android.ddmuilib.logcat.ILogCatMessageSelectionListener; -import com.android.ddmuilib.logcat.LogCatPanel; -import com.android.ddmuilib.logcat.LogCatStackTraceParser; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.JavaSourceRevealer; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.layout.FillLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.actions.ActionFactory; - -public class LogCatView extends SelectionDependentViewPart { - /** LogCatView ID as defined in plugin.xml. */ - public static final String ID = "org.eclipse.andmore.ddms.views.LogCatView"; //$NON-NLS-1$ - - /** Switch perspective when a Java file is opened from logcat view. */ - public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; - - /** Target perspective to open when a Java file is opened from logcat view. */ - public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ - - private LogCatPanel mLogCatPanel; - private LogCatStackTraceParser mStackTraceParser = new LogCatStackTraceParser(); - - private Clipboard mClipboard; - - @Override - public void createPartControl(Composite parent) { - parent.setLayout(new FillLayout()); - - IPreferenceStore prefStore = DdmsPlugin.getDefault().getPreferenceStore(); - mLogCatPanel = new LogCatPanel(prefStore); - mLogCatPanel.createPanel(parent); - setSelectionDependentPanel(mLogCatPanel); - - mLogCatPanel.addLogCatMessageSelectionListener(new ILogCatMessageSelectionListener() { - @Override - public void messageDoubleClicked(LogCatMessage m) { - onDoubleClick(m); - } - }); - - mClipboard = new Clipboard(parent.getDisplay()); - IActionBars actionBars = getViewSite().getActionBars(); - actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { - @Override - public void run() { - mLogCatPanel.copySelectionToClipboard(mClipboard); - } - }); - - actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { - @Override - public void run() { - mLogCatPanel.selectAll(); - } - }); - - actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), new Action("Find") { - @Override - public void run() { - mLogCatPanel.showFindDialog(); - } - }); - } - - @Override - public void setFocus() { - } - - private void onDoubleClick(LogCatMessage m) { - String msg = m.getMessage(); - if (!mStackTraceParser.isValidExceptionTrace(msg)) { - return; - } - - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String perspectiveId = null; - if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { - perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); - } - - String fileName = mStackTraceParser.getFileName(msg); - int lineNumber = mStackTraceParser.getLineNumber(msg); - String methodName = mStackTraceParser.getMethodName(msg); - JavaSourceRevealer.revealMethod(methodName, fileName, lineNumber, perspectiveId); - } - - public void selectTransientAppFilter(String appName) { - mLogCatPanel.selectTransientAppFilter(appName); - } -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmuilib.logcat.ILogCatMessageSelectionListener; +import com.android.ddmuilib.logcat.LogCatPanel; +import com.android.ddmuilib.logcat.LogCatStackTraceParser; + +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.JavaSourceRevealer; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.actions.ActionFactory; + +public class LogCatView extends SelectionDependentViewPart { + /** LogCatView ID as defined in plugin.xml. */ + public static final String ID = "org.eclipse.andmore.ddms.views.LogCatView"; //$NON-NLS-1$ + + /** Switch perspective when a Java file is opened from logcat view. */ + public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; + + /** Target perspective to open when a Java file is opened from logcat view. */ + public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ + + private LogCatPanel mLogCatPanel; + private LogCatStackTraceParser mStackTraceParser = new LogCatStackTraceParser(); + + private Clipboard mClipboard; + + @Override + public void createPartControl(Composite parent) { + parent.setLayout(new FillLayout()); + + IPreferenceStore prefStore = DdmsPlugin.getDefault().getPreferenceStore(); + mLogCatPanel = new LogCatPanel(prefStore, DdmsPlugin.getDefault().getImageFactory()); + mLogCatPanel.createPanel(parent); + setSelectionDependentPanel(mLogCatPanel); + + mLogCatPanel.addLogCatMessageSelectionListener(new ILogCatMessageSelectionListener() { + @Override + public void messageDoubleClicked(LogCatMessage m) { + onDoubleClick(m); + } + }); + + mClipboard = new Clipboard(parent.getDisplay()); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { + @Override + public void run() { + mLogCatPanel.copySelectionToClipboard(mClipboard); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { + @Override + public void run() { + mLogCatPanel.selectAll(); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), new Action("Find") { + @Override + public void run() { + mLogCatPanel.showFindDialog(); + } + }); + } + + @Override + public void setFocus() { + } + + private void onDoubleClick(LogCatMessage m) { + String msg = m.getMessage(); + if (!mStackTraceParser.isValidExceptionTrace(msg)) { + return; + } + + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String perspectiveId = null; + if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { + perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); + } + + String fileName = mStackTraceParser.getFileName(msg); + int lineNumber = mStackTraceParser.getLineNumber(msg); + String methodName = mStackTraceParser.getMethodName(msg); + JavaSourceRevealer.revealMethod(methodName, fileName, lineNumber, perspectiveId); + } + + public void selectTransientAppFilter(String appName) { + mLogCatPanel.selectTransientAppFilter(appName); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java index 4a4b5c16..51a7bb54 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java @@ -1,371 +1,371 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.Log.LogLevel; -import com.android.ddmuilib.ImageLoader; -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.logcat.LogPanel.LogCatViewInterface; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IMarker; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.Font; -import org.eclipse.swt.graphics.FontData; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.IPerspectiveRegistry; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.WorkbenchException; -import org.eclipse.ui.actions.ActionFactory; -import org.eclipse.ui.ide.IDE; - -import java.util.ArrayList; - -/** - * The log cat view displays log output from the current device selection. - */ -public final class OldLogCatView extends SelectionDependentViewPart implements LogCatViewInterface { - - public static final String ID = "org.eclipse.andmore.ddms.views.OldLogCatView"; //$NON-NLS-1$ - - private static final String PREFS_COL_TIME = DdmsPlugin.PLUGIN_ID + ".logcat.time"; //$NON-NLS-1$ - private static final String PREFS_COL_LEVEL = DdmsPlugin.PLUGIN_ID + ".logcat.level"; //$NON-NLS-1$ - private static final String PREFS_COL_PID = DdmsPlugin.PLUGIN_ID + ".logcat.pid"; //$NON-NLS-1$ - private static final String PREFS_COL_TAG = DdmsPlugin.PLUGIN_ID + ".logcat.tag"; //$NON-NLS-1$ - private static final String PREFS_COL_MESSAGE = DdmsPlugin.PLUGIN_ID + ".logcat.message"; //$NON-NLS-1$ - - private static final String PREFS_FILTERS = DdmsPlugin.PLUGIN_ID + ".logcat.filters"; //$NON-NLS-1$ - - public static final String CHOICE_METHOD_DECLARATION = DdmsPlugin.PLUGIN_ID + ".logcat.MethodDeclaration"; //$NON-NLS-1$ - public static final String CHOICE_ERROR_LINE = DdmsPlugin.PLUGIN_ID + ".logcat.ErrorLine"; //$NON-NLS-1$ - - /* Default values for the switch of perspective. */ - public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; - public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ - private static OldLogCatView sThis; - private LogPanel mLogPanel; - - private CommonAction mCreateFilterAction; - private CommonAction mDeleteFilterAction; - private CommonAction mEditFilterAction; - private CommonAction mExportAction; - - private CommonAction[] 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 - }; - - private Action mClearAction; - - private Clipboard mClipboard; - - /** - * An implementation of {@link ILogFilterStorageManager} to bridge to the - * eclipse preference store, and saves the log filters. - */ - private final class FilterStorage implements ILogFilterStorageManager { - - @Override - public LogFilter[] getFilterFromStore() { - String filterPrefs = DdmsPlugin.getDefault().getPreferenceStore().getString(PREFS_FILTERS); - - // split in a string per filter - String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$ - - ArrayList list = new ArrayList(filters.length); - - for (String f : filters) { - if (f.length() > 0) { - LogFilter logFilter = new LogFilter(); - if (logFilter.loadFromString(f)) { - list.add(logFilter); - } - } - } - - return list.toArray(new LogFilter[list.size()]); - } - - @Override - public void saveFilters(LogFilter[] filters) { - StringBuilder sb = new StringBuilder(); - for (LogFilter f : filters) { - String filterString = f.toString(); - sb.append(filterString); - sb.append('|'); - } - - DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString()); - } - - @Override - public boolean requiresDefaultFilter() { - return true; - } - } - - public OldLogCatView() { - sThis = this; - 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; - } - - /** - * Returns the singleton instance. - */ - public static OldLogCatView getInstance() { - return sThis; - } - - /** - * Sets the display font. - * - * @param font - * The font. - */ - public static void setFont(Font font) { - if (sThis != null && sThis.mLogPanel != null) { - sThis.mLogPanel.setFont(font); - } - } - - @Override - public void createPartControl(Composite parent) { - Display d = parent.getDisplay(); - LogColors colors = new LogColors(); - - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - colors.infoColor = new Color(d, 0, 127, 0); - colors.debugColor = new Color(d, 0, 0, 127); - colors.errorColor = new Color(d, 255, 0, 0); - colors.warningColor = new Color(d, 255, 127, 0); - colors.verboseColor = new Color(d, 0, 0, 0); - - mCreateFilterAction = new CommonAction(Messages.LogCatView_Create_Filter) { - @Override - public void run() { - mLogPanel.addFilter(); - } - }; - mCreateFilterAction.setToolTipText(Messages.LogCatView_Create_Filter_Tooltip); - mCreateFilterAction.setImageDescriptor(loader.loadDescriptor("add.png")); //$NON-NLS-1$ - - mEditFilterAction = new CommonAction(Messages.LogCatView_Edit_Filter) { - @Override - public void run() { - mLogPanel.editFilter(); - } - }; - mEditFilterAction.setToolTipText(Messages.LogCatView_Edit_Filter_Tooltip); - mEditFilterAction.setImageDescriptor(loader.loadDescriptor("edit.png")); //$NON-NLS-1$ - - mDeleteFilterAction = new CommonAction(Messages.LogCatView_Delete_Filter) { - @Override - public void run() { - mLogPanel.deleteFilter(); - } - }; - mDeleteFilterAction.setToolTipText(Messages.LogCatView_Delete_Filter_Tooltip); - mDeleteFilterAction.setImageDescriptor(loader.loadDescriptor("delete.png")); //$NON-NLS-1$ - - mExportAction = new CommonAction(Messages.LogCatView_Export_Selection_As_Text) { - @Override - public void run() { - mLogPanel.save(); - } - }; - mExportAction.setToolTipText(Messages.LogCatView_Export_Selection_As_Text_Tooltip); - mExportAction.setImageDescriptor(loader.loadDescriptor("save.png")); //$NON-NLS-1$ - - LogLevel[] levels = LogLevel.values(); - mLogLevelActions = new CommonAction[mLogLevelIcons.length]; - for (int i = 0; i < mLogLevelActions.length; i++) { - String name = levels[i].getStringValue(); - mLogLevelActions[i] = new CommonAction(name, IAction.AS_CHECK_BOX) { - @Override - public void run() { - // disable the other actions and record current index - for (int j = 0; j < mLogLevelActions.length; j++) { - Action a = mLogLevelActions[j]; - if (a == this) { - a.setChecked(true); - - // set the log level - mLogPanel.setCurrentFilterLogLevel(j + 2); - } else { - a.setChecked(false); - } - } - } - }; - - mLogLevelActions[i].setToolTipText(name); - mLogLevelActions[i].setImageDescriptor(loader.loadDescriptor(mLogLevelIcons[i])); - } - - mClearAction = new Action(Messages.LogCatView_Clear_Log) { - @Override - public void run() { - mLogPanel.clear(); - } - }; - mClearAction.setImageDescriptor(loader.loadDescriptor("clear.png")); //$NON-NLS-1$ - - // now create the log view - mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL); - mLogPanel.setLogCatViewInterface(this); - mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); - - // get the font - String fontStr = DdmsPlugin.getDefault().getPreferenceStore().getString(PreferenceInitializer.ATTR_LOGCAT_FONT); - if (fontStr != null) { - FontData data = new FontData(fontStr); - - if (fontStr != null) { - mLogPanel.setFont(new Font(parent.getDisplay(), data)); - } - } - - mLogPanel.createPanel(parent); - setSelectionDependentPanel(mLogPanel); - - // place the actions. - placeActions(); - - // setup the copy action - mClipboard = new Clipboard(d); - IActionBars actionBars = getViewSite().getActionBars(); - actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { - @Override - public void run() { - mLogPanel.copy(mClipboard); - } - }); - - // setup the select all action - actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { - @Override - public void run() { - mLogPanel.selectAll(); - } - }); - } - - @Override - public void dispose() { - mLogPanel.stopLogCat(true); - mClipboard.dispose(); - } - - @Override - public void setFocus() { - mLogPanel.setFocus(); - } - - /** - * Place the actions in the ui. - */ - private void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.add(mCreateFilterAction); - menuManager.add(mEditFilterAction); - menuManager.add(mDeleteFilterAction); - menuManager.add(new Separator()); - menuManager.add(mClearAction); - menuManager.add(new Separator()); - menuManager.add(mExportAction); - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - for (CommonAction a : mLogLevelActions) { - toolBarManager.add(a); - } - toolBarManager.add(new Separator()); - toolBarManager.add(mCreateFilterAction); - toolBarManager.add(mEditFilterAction); - toolBarManager.add(mDeleteFilterAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mClearAction); - } - - void openFile(IFile file, IMarker marker) { - try { - IWorkbenchPage page = getViewSite().getWorkbenchWindow().getActivePage(); - if (page != null) { - IDE.openEditor(page, marker); - marker.delete(); - } - } catch (CoreException e) { - Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e); - DdmsPlugin.getDefault().getLog().log(s); - } - } - - void switchPerspective() { - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { - IWorkbench workbench = PlatformUI.getWorkbench(); - IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); - IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); - String perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); - if (perspectiveId != null && perspectiveId.length() > 0 - && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { - try { - workbench.showPerspective(perspectiveId, window); - } catch (WorkbenchException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public void onDoubleClick() { - } -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.Log.LogLevel; +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.logcat.LogPanel.LogCatViewInterface; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IPerspectiveRegistry; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.WorkbenchException; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.ide.IDE; + +import java.util.ArrayList; + +/** + * The log cat view displays log output from the current device selection. + */ +public final class OldLogCatView extends SelectionDependentViewPart implements LogCatViewInterface { + + public static final String ID = "org.eclipse.andmore.ddms.views.OldLogCatView"; //$NON-NLS-1$ + + private static final String PREFS_COL_TIME = DdmsPlugin.PLUGIN_ID + ".logcat.time"; //$NON-NLS-1$ + private static final String PREFS_COL_LEVEL = DdmsPlugin.PLUGIN_ID + ".logcat.level"; //$NON-NLS-1$ + private static final String PREFS_COL_PID = DdmsPlugin.PLUGIN_ID + ".logcat.pid"; //$NON-NLS-1$ + private static final String PREFS_COL_TAG = DdmsPlugin.PLUGIN_ID + ".logcat.tag"; //$NON-NLS-1$ + private static final String PREFS_COL_MESSAGE = DdmsPlugin.PLUGIN_ID + ".logcat.message"; //$NON-NLS-1$ + + private static final String PREFS_FILTERS = DdmsPlugin.PLUGIN_ID + ".logcat.filters"; //$NON-NLS-1$ + + public static final String CHOICE_METHOD_DECLARATION = DdmsPlugin.PLUGIN_ID + ".logcat.MethodDeclaration"; //$NON-NLS-1$ + public static final String CHOICE_ERROR_LINE = DdmsPlugin.PLUGIN_ID + ".logcat.ErrorLine"; //$NON-NLS-1$ + + /* Default values for the switch of perspective. */ + public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; + public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ + private static OldLogCatView sThis; + private LogPanel mLogPanel; + + private CommonAction mCreateFilterAction; + private CommonAction mDeleteFilterAction; + private CommonAction mEditFilterAction; + private CommonAction mExportAction; + + private CommonAction[] 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 + }; + + private Action mClearAction; + private ImageFactory mImageFactory; + private Clipboard mClipboard; + + /** + * An implementation of {@link ILogFilterStorageManager} to bridge to the + * eclipse preference store, and saves the log filters. + */ + private final class FilterStorage implements ILogFilterStorageManager { + + @Override + public LogFilter[] getFilterFromStore() { + String filterPrefs = DdmsPlugin.getDefault().getPreferenceStore().getString(PREFS_FILTERS); + + // split in a string per filter + String[] filters = filterPrefs.split("\\|"); //$NON-NLS-1$ + + ArrayList list = new ArrayList(filters.length); + + for (String f : filters) { + if (f.length() > 0) { + LogFilter logFilter = new LogFilter(); + if (logFilter.loadFromString(f)) { + list.add(logFilter); + } + } + } + + return list.toArray(new LogFilter[list.size()]); + } + + @Override + public void saveFilters(LogFilter[] filters) { + StringBuilder sb = new StringBuilder(); + for (LogFilter f : filters) { + String filterString = f.toString(); + sb.append(filterString); + sb.append('|'); + } + + DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString()); + } + + @Override + public boolean requiresDefaultFilter() { + return true; + } + } + + public OldLogCatView(ImageFactory imageFactory) { + sThis = this; + mImageFactory = imageFactory; + 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; + } + + /** + * Returns the singleton instance. + */ + public static OldLogCatView getInstance() { + return sThis; + } + + /** + * Sets the display font. + * + * @param font + * The font. + */ + public static void setFont(Font font) { + if (sThis != null && sThis.mLogPanel != null) { + sThis.mLogPanel.setFont(font); + } + } + + @Override + public void createPartControl(Composite parent) { + Display d = parent.getDisplay(); + LogColors colors = new LogColors(); + + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + colors.infoColor = new Color(d, 0, 127, 0); + colors.debugColor = new Color(d, 0, 0, 127); + colors.errorColor = new Color(d, 255, 0, 0); + colors.warningColor = new Color(d, 255, 127, 0); + colors.verboseColor = new Color(d, 0, 0, 0); + + mCreateFilterAction = new CommonAction(Messages.LogCatView_Create_Filter) { + @Override + public void run() { + mLogPanel.addFilter(); + } + }; + mCreateFilterAction.setToolTipText(Messages.LogCatView_Create_Filter_Tooltip); + mCreateFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("add.png")); //$NON-NLS-1$ + + mEditFilterAction = new CommonAction(Messages.LogCatView_Edit_Filter) { + @Override + public void run() { + mLogPanel.editFilter(); + } + }; + mEditFilterAction.setToolTipText(Messages.LogCatView_Edit_Filter_Tooltip); + mEditFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("edit.png")); //$NON-NLS-1$ + + mDeleteFilterAction = new CommonAction(Messages.LogCatView_Delete_Filter) { + @Override + public void run() { + mLogPanel.deleteFilter(); + } + }; + mDeleteFilterAction.setToolTipText(Messages.LogCatView_Delete_Filter_Tooltip); + mDeleteFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("delete.png")); //$NON-NLS-1$ + + mExportAction = new CommonAction(Messages.LogCatView_Export_Selection_As_Text) { + @Override + public void run() { + mLogPanel.save(); + } + }; + mExportAction.setToolTipText(Messages.LogCatView_Export_Selection_As_Text_Tooltip); + mExportAction.setImageDescriptor(imageFactory.getDescriptorByName("save.png")); //$NON-NLS-1$ + + LogLevel[] levels = LogLevel.values(); + mLogLevelActions = new CommonAction[mLogLevelIcons.length]; + for (int i = 0; i < mLogLevelActions.length; i++) { + String name = levels[i].getStringValue(); + mLogLevelActions[i] = new CommonAction(name, IAction.AS_CHECK_BOX) { + @Override + public void run() { + // disable the other actions and record current index + for (int j = 0; j < mLogLevelActions.length; j++) { + Action a = mLogLevelActions[j]; + if (a == this) { + a.setChecked(true); + + // set the log level + mLogPanel.setCurrentFilterLogLevel(j + 2); + } else { + a.setChecked(false); + } + } + } + }; + + mLogLevelActions[i].setToolTipText(name); + mLogLevelActions[i].setImageDescriptor(imageFactory.getDescriptorByName(mLogLevelIcons[i])); + } + + mClearAction = new Action(Messages.LogCatView_Clear_Log) { + @Override + public void run() { + mLogPanel.clear(); + } + }; + mClearAction.setImageDescriptor(imageFactory.getDescriptorByName("clear.png")); //$NON-NLS-1$ + + // now create the log view + mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL, mImageFactory); + mLogPanel.setLogCatViewInterface(this); + mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); + + // get the font + String fontStr = DdmsPlugin.getDefault().getPreferenceStore().getString(PreferenceInitializer.ATTR_LOGCAT_FONT); + if (fontStr != null) { + FontData data = new FontData(fontStr); + + if (fontStr != null) { + mLogPanel.setFont(new Font(parent.getDisplay(), data)); + } + } + + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + + // place the actions. + placeActions(); + + // setup the copy action + mClipboard = new Clipboard(d); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { + @Override + public void run() { + mLogPanel.copy(mClipboard); + } + }); + + // setup the select all action + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { + @Override + public void run() { + mLogPanel.selectAll(); + } + }); + } + + @Override + public void dispose() { + mLogPanel.stopLogCat(true); + mClipboard.dispose(); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + /** + * Place the actions in the ui. + */ + private void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(mCreateFilterAction); + menuManager.add(mEditFilterAction); + menuManager.add(mDeleteFilterAction); + menuManager.add(new Separator()); + menuManager.add(mClearAction); + menuManager.add(new Separator()); + menuManager.add(mExportAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + for (CommonAction a : mLogLevelActions) { + toolBarManager.add(a); + } + toolBarManager.add(new Separator()); + toolBarManager.add(mCreateFilterAction); + toolBarManager.add(mEditFilterAction); + toolBarManager.add(mDeleteFilterAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mClearAction); + } + + void openFile(IFile file, IMarker marker) { + try { + IWorkbenchPage page = getViewSite().getWorkbenchWindow().getActivePage(); + if (page != null) { + IDE.openEditor(page, marker); + marker.delete(); + } + } catch (CoreException e) { + Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e); + DdmsPlugin.getDefault().getLog().log(s); + } + } + + void switchPerspective() { + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); + String perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); + if (perspectiveId != null && perspectiveId.length() > 0 + && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { + try { + workbench.showPerspective(perspectiveId, window); + } catch (WorkbenchException e) { + e.printStackTrace(); + } + } + } + } + + @Override + public void onDoubleClick() { + } +} diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath b/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath index 2abf4833..0c695ecb 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath +++ b/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath @@ -1,11 +1,11 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath b/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath index 2edb66f5..da24a626 100755 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath @@ -1,9 +1,9 @@ - - - - - - - - - + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +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=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF index 1d210493..d766990e 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF @@ -1,23 +1,24 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Tracer for OpenGL ES -Bundle-SymbolicName: org.eclipse.andmore.gldebugger;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.gltrace.GlTracePlugin -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.ide, - org.eclipse.core.resources, - org.eclipse.core.filesystem, - org.eclipse.ui.console, - org.eclipse.andmore.ddms, - org.eclipse.andmore.base -Bundle-ActivationPolicy: lazy -Bundle-ClassPath: libs/host-libprotobuf-java-2.3.0-lite.jar, - libs/liblzf-1.0.jar, - . -Bundle-Vendor: Eclipse Andmore -Export-Package: org.eclipse.andmore.gltrace;x-friends:="com.android.ide.eclipse.gldebugger.tests", - org.eclipse.andmore.gltrace.format;x-friends:="com.android.ide.eclipse.gldebugger.tests", - org.eclipse.andmore.gltrace.model;x-friends:="com.android.ide.eclipse.gldebugger.tests" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Tracer for OpenGL ES +Bundle-SymbolicName: org.eclipse.andmore.gldebugger;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.gltrace.GlTracePlugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.ide, + org.eclipse.core.resources, + org.eclipse.core.filesystem, + org.eclipse.ui.console, + org.eclipse.andmore.swt, + org.eclipse.andmore.ddms, + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2" +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: libs/host-libprotobuf-java-2.3.0-lite.jar, + libs/liblzf-1.0.jar, + . +Bundle-Vendor: Eclipse Andmore +Export-Package: org.eclipse.andmore.gltrace;x-friends:="com.android.ide.eclipse.gldebugger.tests", + org.eclipse.andmore.gltrace.format;x-friends:="com.android.ide.eclipse.gldebugger.tests", + org.eclipse.andmore.gltrace.model;x-friends:="com.android.ide.eclipse.gldebugger.tests" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath index 3d094dff..7498423d 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath @@ -1,8 +1,7 @@ - - - - - - - - + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +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=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF index 8067225a..df622fd1 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF @@ -1,18 +1,19 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.console, - org.eclipse.andmore.ddms, - org.eclipse.andmore.base -Bundle-ClassPath: ., - libs/hierarchyviewer2lib.jar -Export-Package: org.eclipse.andmore.hierarchyviewer -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.console, + org.eclipse.andmore.swt, + org.eclipse.andmore.ddms, + org.eclipse.andmore.hierarchyviewer2lib, + org.eclipse.andmore.ddmuilib +Bundle-ClassPath: . +Export-Package: org.eclipse.andmore.hierarchyviewer +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties index 9fb508f2..7a8922c6 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties @@ -1,11 +1,11 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - icons/,\ - plugin.xml,\ - .,\ - libs/,\ - about.html,\ - about.ini,\ - about.properties,\ - plugin.properties +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + icons/,\ + plugin.xml,\ + .,\ + about.html,\ + about.ini,\ + about.properties,\ + plugin.properties,\ + hiarch/ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/auto-refresh.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/auto-refresh.png new file mode 100644 index 00000000..240862f3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/auto-refresh.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/capture-psd.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/capture-psd.png new file mode 100644 index 00000000..0f25426b Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/capture-psd.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view-selected.png new file mode 100644 index 00000000..fd107ed9 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view-selected.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view.png new file mode 100644 index 00000000..9a7eed49 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device.png new file mode 100644 index 00000000..7dbbbb6a Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/display.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/display.png new file mode 100644 index 00000000..a9de0ec7 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/display.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png new file mode 100644 index 00000000..a7180428 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/file.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/file.png new file mode 100644 index 00000000..043a8143 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/file.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/filtered.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/filtered.png new file mode 100644 index 00000000..4fcab3f3 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/filtered.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/folder.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/folder.png new file mode 100644 index 00000000..7e29b1a4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/folder.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/green.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/green.png new file mode 100644 index 00000000..800000db Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/green.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/inspect-screenshot.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/inspect-screenshot.png new file mode 100644 index 00000000..6e517012 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/inspect-screenshot.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/invalidate.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/invalidate.png new file mode 100644 index 00000000..ee75f695 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/invalidate.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-all-views.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-all-views.png new file mode 100644 index 00000000..3329ec9d Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-all-views.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-overlay.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-overlay.png new file mode 100644 index 00000000..48172525 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-overlay.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-view-hierarchy.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-view-hierarchy.png new file mode 100644 index 00000000..8f01dda4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-view-hierarchy.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/not-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/not-selected.png new file mode 100644 index 00000000..db6f13b4 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/not-selected.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-black.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-black.png new file mode 100644 index 00000000..cd888031 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-black.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-white.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-white.png new file mode 100644 index 00000000..5f05662c Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-white.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/picker.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/picker.png new file mode 100644 index 00000000..8ea2bed6 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/picker.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view-selected.png new file mode 100644 index 00000000..1e44000f Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view-selected.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view.png new file mode 100644 index 00000000..ec51cec0 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/profile.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/profile.png new file mode 100644 index 00000000..1e9fb5ab Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/profile.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/red.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/red.png new file mode 100644 index 00000000..a2ab8554 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/red.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/refresh-windows.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/refresh-windows.png new file mode 100644 index 00000000..8fddcaed Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/refresh-windows.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/request-layout.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/request-layout.png new file mode 100644 index 00000000..92a78c81 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/request-layout.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/save.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/save.png new file mode 100644 index 00000000..2c0bab1e Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/save.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-128.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-128.png new file mode 100644 index 00000000..4535f229 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-128.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-16.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-16.png new file mode 100644 index 00000000..8c3c23dd Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-16.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered-small.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered-small.png new file mode 100644 index 00000000..9ef6b343 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered-small.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered.png new file mode 100644 index 00000000..1f59685b Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-small.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-small.png new file mode 100644 index 00000000..538e3853 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-small.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected.png new file mode 100644 index 00000000..5cd5c3fc Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-extras.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-extras.png new file mode 100644 index 00000000..ba9c305c Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-extras.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-overlay.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-overlay.png new file mode 100644 index 00000000..e39e90a1 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-overlay.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view-selected.png new file mode 100644 index 00000000..175ad1f0 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view-selected.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view.png new file mode 100644 index 00000000..23aa4244 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/yellow.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/yellow.png new file mode 100644 index 00000000..e9b57813 Binary files /dev/null and b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/yellow.png differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar deleted file mode 100644 index a339050e..00000000 Binary files a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar and /dev/null differ diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HiarchResourceProvider.java b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HiarchResourceProvider.java new file mode 100644 index 00000000..f237555e --- /dev/null +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HiarchResourceProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.hierarchyviewer; + +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.jface.resource.ImageDescriptor; + +public class HiarchResourceProvider implements PluginResourceProvider { + + @Override + public ImageDescriptor descriptorFromPath(String imagePath) { + return HierarchyViewerPlugin.getImageDescriptor("hiarch/" + imagePath); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPlugin.java b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPlugin.java index 01fa81ae..5e737cc7 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPlugin.java +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPlugin.java @@ -23,7 +23,9 @@ import com.android.ddmlib.Log.LogLevel; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; @@ -47,6 +49,7 @@ public class HierarchyViewerPlugin extends AbstractUIPlugin { // The shared instance private static HierarchyViewerPlugin sPlugin; + private HierarchyViewerDirector director; private Color mRedColor; @@ -82,7 +85,7 @@ public void run() { }); // set up the ddms log to use the ddms console. - Log.setLogOutput(new ILogOutput() { + Log.addLogger(new ILogOutput() { @Override public void printLog(LogLevel logLevel, String tag, String message) { if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { @@ -112,7 +115,7 @@ public void run() { }); - final HierarchyViewerDirector director = HierarchyViewerPluginDirector.createDirector(); + director = HierarchyViewerPluginDirector.createDirector(); director.startListenForDevices(); // make the director receive change in ADB. @@ -149,7 +152,6 @@ public void stop(BundleContext context) throws Exception { mRedColor.dispose(); - HierarchyViewerDirector director = HierarchyViewerDirector.getDirector(); director.stopListenForDevices(); director.stopDebugBridge(); director.terminate(); @@ -164,6 +166,13 @@ public static HierarchyViewerPlugin getPlugin() { return sPlugin; } + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } /** * Prints a message, associated with a project to the specified stream * @@ -197,4 +206,9 @@ private static String getMessageTag(String tag) { return String.format("[%1$tF %1$tT - %2$s]", c, tag); //$NON-NLS-1$ } + + public ImageFactory getImageFactory() { + // Director is not expected to be null because of plugin lifecycle + return director != null ? director.getImageFactory() : null; + } } diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPluginDirector.java b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPluginDirector.java index e6f36fde..c3c953d0 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPluginDirector.java +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HierarchyViewerPluginDirector.java @@ -1,119 +1,126 @@ -/* - * 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 org.eclipse.andmore.hierarchyviewer; - -import com.android.hierarchyviewerlib.HierarchyViewerDirector; -import com.android.hierarchyviewerlib.device.IHvDevice; -import com.android.hierarchyviewerlib.models.Window; - -import org.eclipse.andmore.hierarchyviewer.views.PixelPerfectTreeView; -import org.eclipse.andmore.hierarchyviewer.views.PropertyView; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.jobs.ISchedulingRule; -import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchPart; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.PartInitException; - -public class HierarchyViewerPluginDirector extends HierarchyViewerDirector { - - public static HierarchyViewerDirector createDirector() { - return sDirector = new HierarchyViewerPluginDirector(); - } - - @Override - public void executeInBackground(final String taskName, final Runnable task) { - Job job = new Job(taskName) { - @Override - protected IStatus run(IProgressMonitor monitor) { - task.run(); - return Status.OK_STATUS; - } - }; - job.setPriority(Job.SHORT); - job.setRule(mSchedulingRule); - job.schedule(); - } - - private ISchedulingRule mSchedulingRule = new ISchedulingRule() { - @Override - public boolean contains(ISchedulingRule rule) { - return rule == this; - } - - @Override - public boolean isConflicting(ISchedulingRule rule) { - return rule == this; - } - - }; - - @Override - public String getAdbLocation() { - return HierarchyViewerPlugin.getPlugin().getPreferenceStore().getString(HierarchyViewerPlugin.ADB_LOCATION); - } - - @Override - public void loadViewTreeData(Window window) { - super.loadViewTreeData(window); - - // The windows tab hides the property tab, so let's bring the property - // tab - // forward. - - IWorkbenchWindow[] windows = HierarchyViewerPlugin.getPlugin().getWorkbench().getWorkbenchWindows(); - for (IWorkbenchWindow currentWindow : windows) { - IWorkbenchPage page = currentWindow.getActivePage(); - if (page.getPerspective().getId().equals(TreeViewPerspective.ID)) { - try { - IWorkbenchPart part = page.findView(PropertyView.ID); - if (part != null) { - page.showView(PropertyView.ID); - } - } catch (PartInitException e) { - - } - } - } - } - - @Override - public void loadPixelPerfectData(IHvDevice device) { - super.loadPixelPerfectData(device); - - // The windows tab hides the tree tab, so let's bring the tree tab - // forward. - - IWorkbenchWindow[] windows = HierarchyViewerPlugin.getPlugin().getWorkbench().getWorkbenchWindows(); - for (IWorkbenchWindow window : windows) { - IWorkbenchPage page = window.getActivePage(); - if (page.getPerspective().getId().equals(PixelPerfectPespective.ID)) { - try { - IWorkbenchPart part = page.findView(PixelPerfectTreeView.ID); - if (part != null) { - page.showView(PixelPerfectTreeView.ID); - } - } catch (PartInitException e) { - - } - } - } - } -} +/* + * 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 org.eclipse.andmore.hierarchyviewer; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; +import org.eclipse.andmore.hierarchyviewer.views.PixelPerfectTreeView; +import org.eclipse.andmore.hierarchyviewer.views.PropertyView; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; + +public class HierarchyViewerPluginDirector extends HierarchyViewerDirector { + + public HierarchyViewerPluginDirector(ImageFactory imageFactory) { + super(imageFactory); + // TODO Auto-generated constructor stub + } + + public static HierarchyViewerDirector createDirector() { + return sDirector = new HierarchyViewerPluginDirector(new JFaceImageLoader(new HiarchResourceProvider())); + } + + @Override + public void executeInBackground(final String taskName, final Runnable task) { + Job job = new Job(taskName) { + @Override + protected IStatus run(IProgressMonitor monitor) { + task.run(); + return Status.OK_STATUS; + } + }; + job.setPriority(Job.SHORT); + job.setRule(mSchedulingRule); + job.schedule(); + } + + private ISchedulingRule mSchedulingRule = new ISchedulingRule() { + @Override + public boolean contains(ISchedulingRule rule) { + return rule == this; + } + + @Override + public boolean isConflicting(ISchedulingRule rule) { + return rule == this; + } + + }; + + @Override + public String getAdbLocation() { + return HierarchyViewerPlugin.getPlugin().getPreferenceStore().getString(HierarchyViewerPlugin.ADB_LOCATION); + } + + @Override + public void loadViewTreeData(Window window) { + super.loadViewTreeData(window); + + // The windows tab hides the property tab, so let's bring the property + // tab + // forward. + + IWorkbenchWindow[] windows = HierarchyViewerPlugin.getPlugin().getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow currentWindow : windows) { + IWorkbenchPage page = currentWindow.getActivePage(); + if (page.getPerspective().getId().equals(TreeViewPerspective.ID)) { + try { + IWorkbenchPart part = page.findView(PropertyView.ID); + if (part != null) { + page.showView(PropertyView.ID); + } + } catch (PartInitException e) { + + } + } + } + } + + @Override + public void loadPixelPerfectData(IHvDevice device) { + super.loadPixelPerfectData(device); + + // The windows tab hides the tree tab, so let's bring the tree tab + // forward. + + IWorkbenchWindow[] windows = HierarchyViewerPlugin.getPlugin().getWorkbench().getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + IWorkbenchPage page = window.getActivePage(); + if (page.getPerspective().getId().equals(PixelPerfectPespective.ID)) { + try { + IWorkbenchPart part = page.findView(PixelPerfectTreeView.ID); + if (part != null) { + page.showView(PixelPerfectTreeView.ID); + } + } catch (PartInitException e) { + + } + } + } + } +} diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/LayoutView.java b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/LayoutView.java index c9f6b29a..55a620d4 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/LayoutView.java +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/LayoutView.java @@ -1,160 +1,161 @@ -/* - * 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 org.eclipse.andmore.hierarchyviewer.views; - -import com.android.ddmuilib.ImageLoader; -import com.android.hierarchyviewerlib.HierarchyViewerDirector; -import com.android.hierarchyviewerlib.models.TreeViewModel; -import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; -import com.android.hierarchyviewerlib.ui.LayoutViewer; - -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.layout.FillLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.part.ViewPart; - -public class LayoutView extends ViewPart implements ITreeChangeListener { - - public static final String ID = "org.eclipse.andmore.hierarchyviewer.views.LayoutView"; //$NON-NLS-1$ - - private LayoutViewer mLayoutViewer; - - private Image mOnBlack; - - private Image mOnWhite; - - private Action mShowExtrasAction = new Action("Show &Extras", Action.AS_CHECK_BOX) { - @Override - public void run() { - mLayoutViewer.setShowExtras(isChecked()); - } - }; - - private Action mLoadAllViewsAction = new Action("Load All &Views") { - @Override - public void run() { - HierarchyViewerDirector.getDirector().loadAllViews(); - mShowExtrasAction.setChecked(true); - mLayoutViewer.setShowExtras(true); - } - }; - - private Action mOnBlackWhiteAction = new Action("Change Background &Color") { - @Override - public void run() { - boolean newValue = !mLayoutViewer.getOnBlack(); - mLayoutViewer.setOnBlack(newValue); - if (newValue) { - setImageDescriptor(ImageDescriptor.createFromImage(mOnWhite)); - } else { - setImageDescriptor(ImageDescriptor.createFromImage(mOnBlack)); - } - } - }; - - @Override - public void createPartControl(Composite parent) { - mShowExtrasAction.setAccelerator(SWT.MOD1 + 'E'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - Image image = imageLoader.loadImage("show-extras.png", Display.getDefault()); //$NON-NLS-1$ - mShowExtrasAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); - mShowExtrasAction.setToolTipText("Show images"); - mShowExtrasAction.setEnabled(TreeViewModel.getModel().getTree() != null); - - mOnWhite = imageLoader.loadImage("on-white.png", Display.getDefault()); //$NON-NLS-1$ - mOnBlack = imageLoader.loadImage("on-black.png", Display.getDefault()); //$NON-NLS-1$ - - mOnBlackWhiteAction.setAccelerator(SWT.MOD1 + 'C'); - mOnBlackWhiteAction.setImageDescriptor(ImageDescriptor.createFromImage(mOnWhite)); - mOnBlackWhiteAction.setToolTipText("Change layout viewer background color"); - - mLoadAllViewsAction.setAccelerator(SWT.MOD1 + 'V'); - image = imageLoader.loadImage("load-all-views.png", Display.getDefault()); //$NON-NLS-1$ - mLoadAllViewsAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); - mLoadAllViewsAction.setToolTipText("Load all view images"); - mLoadAllViewsAction.setEnabled(TreeViewModel.getModel().getTree() != null); - - parent.setLayout(new FillLayout()); - - mLayoutViewer = new LayoutViewer(parent); - - placeActions(); - - TreeViewModel.getModel().addTreeChangeListener(this); - } - - public void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - IMenuManager mm = actionBars.getMenuManager(); - mm.removeAll(); - mm.add(mOnBlackWhiteAction); - mm.add(mShowExtrasAction); - mm.add(mLoadAllViewsAction); - - IToolBarManager tm = actionBars.getToolBarManager(); - tm.removeAll(); - tm.add(mOnBlackWhiteAction); - tm.add(mShowExtrasAction); - tm.add(mLoadAllViewsAction); - } - - @Override - public void dispose() { - super.dispose(); - TreeViewModel.getModel().removeTreeChangeListener(this); - } - - @Override - public void setFocus() { - mLayoutViewer.setFocus(); - } - - @Override - public void selectionChanged() { - // pass - } - - @Override - public void treeChanged() { - Display.getDefault().syncExec(new Runnable() { - @Override - public void run() { - mLoadAllViewsAction.setEnabled(TreeViewModel.getModel().getTree() != null); - mShowExtrasAction.setEnabled(TreeViewModel.getModel().getTree() != null); - } - }); - } - - @Override - public void viewportChanged() { - // pass - } - - @Override - public void zoomChanged() { - // pass - } - -} +/* + * 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 org.eclipse.andmore.hierarchyviewer.views; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.ui.LayoutViewer; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.part.ViewPart; + +public class LayoutView extends ViewPart implements ITreeChangeListener { + + public static final String ID = "org.eclipse.andmore.hierarchyviewer.views.LayoutView"; //$NON-NLS-1$ + + private LayoutViewer mLayoutViewer; + + private Image mOnBlack; + + private Image mOnWhite; + + private Action mShowExtrasAction = new Action("Show &Extras", Action.AS_CHECK_BOX) { + @Override + public void run() { + mLayoutViewer.setShowExtras(isChecked()); + } + }; + + private Action mLoadAllViewsAction = new Action("Load All &Views") { + @Override + public void run() { + HierarchyViewerDirector.getDirector().loadAllViews(); + mShowExtrasAction.setChecked(true); + mLayoutViewer.setShowExtras(true); + } + }; + + private Action mOnBlackWhiteAction = new Action("Change Background &Color") { + @Override + public void run() { + boolean newValue = !mLayoutViewer.getOnBlack(); + mLayoutViewer.setOnBlack(newValue); + if (newValue) { + setImageDescriptor(ImageDescriptor.createFromImage(mOnWhite)); + } else { + setImageDescriptor(ImageDescriptor.createFromImage(mOnBlack)); + } + } + }; + + @Override + public void createPartControl(Composite parent) { + mShowExtrasAction.setAccelerator(SWT.MOD1 + 'E'); + ImageFactory imageFactory = HierarchyViewerPlugin.getPlugin().getImageFactory(); + Image image = imageFactory.getImageByName("show-extras.png"); //$NON-NLS-1$ + mShowExtrasAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); + mShowExtrasAction.setToolTipText("Show images"); + mShowExtrasAction.setEnabled(TreeViewModel.getModel().getTree() != null); + + mOnWhite = imageFactory.getImageByName("on-white.png"); //$NON-NLS-1$ + mOnBlack = imageFactory.getImageByName("on-black.png"); //$NON-NLS-1$ + + mOnBlackWhiteAction.setAccelerator(SWT.MOD1 + 'C'); + mOnBlackWhiteAction.setImageDescriptor(ImageDescriptor.createFromImage(mOnWhite)); + mOnBlackWhiteAction.setToolTipText("Change layout viewer background color"); + + mLoadAllViewsAction.setAccelerator(SWT.MOD1 + 'V'); + image = imageFactory.getImageByName("load-all-views.png"); //$NON-NLS-1$ + mLoadAllViewsAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); + mLoadAllViewsAction.setToolTipText("Load all view images"); + mLoadAllViewsAction.setEnabled(TreeViewModel.getModel().getTree() != null); + + parent.setLayout(new FillLayout()); + + mLayoutViewer = new LayoutViewer(parent); + + placeActions(); + + TreeViewModel.getModel().addTreeChangeListener(this); + } + + public void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + IMenuManager mm = actionBars.getMenuManager(); + mm.removeAll(); + mm.add(mOnBlackWhiteAction); + mm.add(mShowExtrasAction); + mm.add(mLoadAllViewsAction); + + IToolBarManager tm = actionBars.getToolBarManager(); + tm.removeAll(); + tm.add(mOnBlackWhiteAction); + tm.add(mShowExtrasAction); + tm.add(mLoadAllViewsAction); + } + + @Override + public void dispose() { + super.dispose(); + TreeViewModel.getModel().removeTreeChangeListener(this); + } + + @Override + public void setFocus() { + mLayoutViewer.setFocus(); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mLoadAllViewsAction.setEnabled(TreeViewModel.getModel().getTree() != null); + mShowExtrasAction.setEnabled(TreeViewModel.getModel().getTree() != null); + } + }); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/PixelPerfectLoupeView.java b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/PixelPerfectLoupeView.java index 583bd0f0..b687e490 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/PixelPerfectLoupeView.java +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/views/PixelPerfectLoupeView.java @@ -1,164 +1,164 @@ -/* - * 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 org.eclipse.andmore.hierarchyviewer.views; - -import com.android.ddmuilib.ImageLoader; -import com.android.hierarchyviewerlib.HierarchyViewerDirector; -import com.android.hierarchyviewerlib.actions.PixelPerfectAutoRefreshAction; -import com.android.hierarchyviewerlib.models.PixelPerfectModel; -import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; -import com.android.hierarchyviewerlib.ui.PixelPerfectControls; -import com.android.hierarchyviewerlib.ui.PixelPerfectLoupe; -import com.android.hierarchyviewerlib.ui.PixelPerfectPixelPanel; - -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.part.ViewPart; - -public class PixelPerfectLoupeView extends ViewPart implements IImageChangeListener { - - public static final String ID = "org.eclipse.andmore.hierarchyviewer.views.PixelPerfectLoupeView"; //$NON-NLS-1$ - - private PixelPerfectLoupe mPixelPerfectLoupe; - - private Action mShowInLoupeAction = new Action("&Show Overlay", Action.AS_CHECK_BOX) { - @Override - public void run() { - mPixelPerfectLoupe.setShowOverlay(isChecked()); - } - }; - - @Override - public void createPartControl(Composite parent) { - mShowInLoupeAction.setAccelerator(SWT.MOD1 + 'S'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - Image image = imageLoader.loadImage("show-overlay.png", Display.getDefault()); //$NON-NLS-1$ - mShowInLoupeAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); - mShowInLoupeAction.setToolTipText("Show the overlay in the loupe view"); - mShowInLoupeAction.setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); - PixelPerfectModel.getModel().addImageChangeListener(this); - - GridLayout loupeLayout = new GridLayout(); - loupeLayout.marginWidth = loupeLayout.marginHeight = 0; - loupeLayout.horizontalSpacing = loupeLayout.verticalSpacing = 0; - parent.setLayout(loupeLayout); - - Composite pixelPerfectLoupeBorder = new Composite(parent, SWT.BORDER); - pixelPerfectLoupeBorder.setLayoutData(new GridData(GridData.FILL_BOTH)); - GridLayout pixelPerfectLoupeBorderGridLayout = new GridLayout(); - pixelPerfectLoupeBorderGridLayout.marginWidth = pixelPerfectLoupeBorderGridLayout.marginHeight = 0; - pixelPerfectLoupeBorderGridLayout.horizontalSpacing = pixelPerfectLoupeBorderGridLayout.verticalSpacing = 0; - pixelPerfectLoupeBorder.setLayout(pixelPerfectLoupeBorderGridLayout); - - mPixelPerfectLoupe = new PixelPerfectLoupe(pixelPerfectLoupeBorder); - mPixelPerfectLoupe.setLayoutData(new GridData(GridData.FILL_BOTH)); - - PixelPerfectPixelPanel pixelPerfectPixelPanel = new PixelPerfectPixelPanel(pixelPerfectLoupeBorder); - pixelPerfectPixelPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - PixelPerfectControls pixelPerfectControls = new PixelPerfectControls(parent); - pixelPerfectControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - placeActions(); - } - - private void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - IMenuManager mm = actionBars.getMenuManager(); - mm.removeAll(); - mm.add(PixelPerfectAutoRefreshAction.getAction()); - mm.add(mShowInLoupeAction); - - IToolBarManager tm = actionBars.getToolBarManager(); - tm.removeAll(); - tm.add(PixelPerfectAutoRefreshAction.getAction()); - tm.add(mShowInLoupeAction); - } - - @Override - public void dispose() { - super.dispose(); - PixelPerfectModel.getModel().removeImageChangeListener(this); - } - - @Override - public void setFocus() { - mPixelPerfectLoupe.setFocus(); - } - - @Override - public void crosshairMoved() { - // pass - } - - @Override - public void treeChanged() { - // pass - } - - @Override - public void imageChanged() { - // pass - } - - @Override - public void imageLoaded() { - Display.getDefault().syncExec(new Runnable() { - @Override - public void run() { - Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); - mShowInLoupeAction.setEnabled(overlayImage != null); - } - }); - } - - @Override - public void overlayChanged() { - Display.getDefault().syncExec(new Runnable() { - @Override - public void run() { - mShowInLoupeAction.setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); - } - }); - } - - @Override - public void overlayTransparencyChanged() { - // pass - } - - @Override - public void selectionChanged() { - // pass - } - - @Override - public void zoomChanged() { - // pass - } - -} +/* + * 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 org.eclipse.andmore.hierarchyviewer.views; + +import com.android.hierarchyviewerlib.actions.PixelPerfectAutoRefreshAction; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; +import com.android.hierarchyviewerlib.ui.PixelPerfectControls; +import com.android.hierarchyviewerlib.ui.PixelPerfectLoupe; +import com.android.hierarchyviewerlib.ui.PixelPerfectPixelPanel; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.part.ViewPart; + +public class PixelPerfectLoupeView extends ViewPart implements IImageChangeListener { + + public static final String ID = "org.eclipse.andmore.hierarchyviewer.views.PixelPerfectLoupeView"; //$NON-NLS-1$ + + private PixelPerfectLoupe mPixelPerfectLoupe; + + private Action mShowInLoupeAction = new Action("&Show Overlay", Action.AS_CHECK_BOX) { + @Override + public void run() { + mPixelPerfectLoupe.setShowOverlay(isChecked()); + } + }; + + @Override + public void createPartControl(Composite parent) { + mShowInLoupeAction.setAccelerator(SWT.MOD1 + 'S'); + ImageFactory imageFactory = HierarchyViewerPlugin.getPlugin().getImageFactory(); + Image image = imageFactory.getImageByName("show-overlay.png"); //$NON-NLS-1$ + mShowInLoupeAction.setImageDescriptor(ImageDescriptor.createFromImage(image)); + mShowInLoupeAction.setToolTipText("Show the overlay in the loupe view"); + mShowInLoupeAction.setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); + PixelPerfectModel.getModel().addImageChangeListener(this); + + GridLayout loupeLayout = new GridLayout(); + loupeLayout.marginWidth = loupeLayout.marginHeight = 0; + loupeLayout.horizontalSpacing = loupeLayout.verticalSpacing = 0; + parent.setLayout(loupeLayout); + + Composite pixelPerfectLoupeBorder = new Composite(parent, SWT.BORDER); + pixelPerfectLoupeBorder.setLayoutData(new GridData(GridData.FILL_BOTH)); + GridLayout pixelPerfectLoupeBorderGridLayout = new GridLayout(); + pixelPerfectLoupeBorderGridLayout.marginWidth = pixelPerfectLoupeBorderGridLayout.marginHeight = 0; + pixelPerfectLoupeBorderGridLayout.horizontalSpacing = pixelPerfectLoupeBorderGridLayout.verticalSpacing = 0; + pixelPerfectLoupeBorder.setLayout(pixelPerfectLoupeBorderGridLayout); + + mPixelPerfectLoupe = new PixelPerfectLoupe(pixelPerfectLoupeBorder); + mPixelPerfectLoupe.setLayoutData(new GridData(GridData.FILL_BOTH)); + + PixelPerfectPixelPanel pixelPerfectPixelPanel = new PixelPerfectPixelPanel(pixelPerfectLoupeBorder); + pixelPerfectPixelPanel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + PixelPerfectControls pixelPerfectControls = new PixelPerfectControls(parent); + pixelPerfectControls.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + placeActions(); + } + + private void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + IMenuManager mm = actionBars.getMenuManager(); + mm.removeAll(); + mm.add(PixelPerfectAutoRefreshAction.getAction()); + mm.add(mShowInLoupeAction); + + IToolBarManager tm = actionBars.getToolBarManager(); + tm.removeAll(); + tm.add(PixelPerfectAutoRefreshAction.getAction()); + tm.add(mShowInLoupeAction); + } + + @Override + public void dispose() { + super.dispose(); + PixelPerfectModel.getModel().removeImageChangeListener(this); + } + + @Override + public void setFocus() { + mPixelPerfectLoupe.setFocus(); + } + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void imageChanged() { + // pass + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mShowInLoupeAction.setEnabled(overlayImage != null); + } + }); + } + + @Override + public void overlayChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mShowInLoupeAction.setEnabled(PixelPerfectModel.getModel().getOverlayImage() != null); + } + }); + } + + @Override + public void overlayTransparencyChanged() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/.classpath b/android-core/plugins/org.eclipse.andmore.integration.tests/.classpath index 6466c26e..30dcc906 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/.classpath +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/.classpath @@ -1,8 +1,8 @@ - - - - - - - - + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.integration.tests/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +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=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +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.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.integration.tests/META-INF/MANIFEST.MF index fa9d4783..856c506c 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/META-INF/MANIFEST.MF @@ -1,19 +1,15 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Android Plugin Tests -Bundle-SymbolicName: org.eclipse.andmore.integration.tests -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: Eclipse Andmore -Fragment-Host: org.eclipse.andmore -Require-Bundle: org.junit, - org.eclipse.andmore.base, - org.eclipse.andmore, - org.easymock, - org.custommonkey.xmlunit, - org.eclipse.andmore.android -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ClassPath: ., - libs/kxml2-2.3.0.jar, - libs/testutils.jar, - libs/lint-api.jar, - libs/lint-checks.jar +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Android Plugin Tests +Bundle-SymbolicName: org.eclipse.andmore.integration.tests +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: Eclipse Andmore +Fragment-Host: org.eclipse.andmore +Require-Bundle: org.junit, + org.eclipse.andmore.swt, + org.eclipse.andmore, + org.easymock, + org.custommonkey.xmlunit, + org.eclipse.andmore.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ClassPath: . diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/IntegrationTestSuite.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/IntegrationTestSuite.java index 87993eb1..f453efbc 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/IntegrationTestSuite.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/IntegrationTestSuite.java @@ -1,63 +1,64 @@ -/** - * Copyright (C) 2015 David Carver and others - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.eclipse.andmore.integration.tests; - -import org.eclipse.andmore.integration.tests.functests.sampleProjects.SampleProjectTest; -import org.eclipse.andmore.internal.build.AaptParserTest; -import org.eclipse.andmore.internal.build.AaptQuickFixTest; -import org.eclipse.andmore.internal.build.DexWrapperTest; -import org.eclipse.andmore.internal.editors.AndroidContentAssistTest; -import org.eclipse.andmore.internal.editors.AndroidXmlAutoEditStrategyTest; -import org.eclipse.andmore.internal.editors.AndroidXmlCharacterMatcherTest; -import org.eclipse.andmore.internal.editors.HyperlinksTest; -import org.eclipse.andmore.internal.editors.formatting.EclipseXmlPrettyPrinterTest; -import org.eclipse.andmore.internal.editors.layout.gle2.LayoutMetadataTest; -import org.eclipse.andmore.internal.editors.manifest.ManifestInfoTest; -import org.eclipse.andmore.internal.launch.JUnitLaunchConfigDelegateTest; -import org.eclipse.andmore.internal.lint.ProjectLintConfigurationTest; -import org.eclipse.andmore.internal.refactorings.core.AndroidPackageRenameParticipantTest; -import org.eclipse.andmore.internal.refactorings.core.RenameResourceParticipantTest; -import org.eclipse.andmore.internal.refactorings.renamepackage.ApplicationPackageNameRefactoringTest; -import org.eclipse.andmore.internal.settings.MultiDexTest; -import org.eclipse.andmore.internal.wizards.exportgradle.ExportGradleTest; -import org.eclipse.andmore.internal.wizards.templates.TemplateHandlerTest; -import org.junit.runner.RunWith; -import org.junit.runners.Suite; - -@RunWith(Suite.class) -@Suite.SuiteClasses({SampleProjectTest.class, - AaptParserTest.class, - AaptQuickFixTest.class, - DexWrapperTest.class, - MultiDexTest.class, - EclipseXmlPrettyPrinterTest.class, - AndroidContentAssistTest.class, - AndroidXmlAutoEditStrategyTest.class, - AndroidXmlCharacterMatcherTest.class, - HyperlinksTest.class, - LayoutMetadataTest.class, - ManifestInfoTest.class, - JUnitLaunchConfigDelegateTest.class, - ProjectLintConfigurationTest.class, - AndroidPackageRenameParticipantTest.class, - RenameResourceParticipantTest.class, - ApplicationPackageNameRefactoringTest.class, - ExportGradleTest.class, - TemplateHandlerTest.class}) -public class IntegrationTestSuite { - -} +/** + * Copyright (C) 2015 David Carver and others + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.andmore.integration.tests; + +import org.eclipse.andmore.integration.tests.functests.sampleProjects.SampleProjectTest; +import org.eclipse.andmore.internal.build.AaptParserTest; +import org.eclipse.andmore.internal.build.AaptQuickFixTest; +//import org.eclipse.andmore.internal.build.DexWrapperTest; +import org.eclipse.andmore.internal.editors.AndroidContentAssistTest; +import org.eclipse.andmore.internal.editors.AndroidXmlAutoEditStrategyTest; +import org.eclipse.andmore.internal.editors.AndroidXmlCharacterMatcherTest; +import org.eclipse.andmore.internal.editors.HyperlinksTest; +import org.eclipse.andmore.internal.editors.formatting.EclipseXmlPrettyPrinterTest; +import org.eclipse.andmore.internal.editors.layout.gle2.LayoutMetadataTest; +import org.eclipse.andmore.internal.editors.manifest.ManifestInfoTest; +import org.eclipse.andmore.internal.launch.JUnitLaunchConfigDelegateTest; +import org.eclipse.andmore.internal.lint.ProjectLintConfigurationTest; +import org.eclipse.andmore.internal.refactorings.core.AndroidPackageRenameParticipantTest; +import org.eclipse.andmore.internal.refactorings.core.RenameResourceParticipantTest; +import org.eclipse.andmore.internal.refactorings.renamepackage.ApplicationPackageNameRefactoringTest; +import org.eclipse.andmore.internal.settings.MultiDexTest; +import org.eclipse.andmore.internal.wizards.exportgradle.ExportGradleTest; +import org.eclipse.andmore.internal.wizards.templates.TemplateHandlerTest; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({SampleProjectTest.class, + AaptParserTest.class, + //AaptQuickFixTest.class, + // DexWrapper only works with build tools version 21 - 25 + //DexWrapperTest.class, + MultiDexTest.class, + EclipseXmlPrettyPrinterTest.class, + AndroidContentAssistTest.class, + AndroidXmlAutoEditStrategyTest.class, + AndroidXmlCharacterMatcherTest.class, + HyperlinksTest.class, + LayoutMetadataTest.class, + ManifestInfoTest.class, + JUnitLaunchConfigDelegateTest.class, + ProjectLintConfigurationTest.class, + AndroidPackageRenameParticipantTest.class, + RenameResourceParticipantTest.class, + ApplicationPackageNameRefactoringTest.class, + ExportGradleTest.class, + TemplateHandlerTest.class}) +public class IntegrationTestSuite { + +} diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkLoadingTestCase.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkLoadingTestCase.java index 10d88dad..89448ecd 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkLoadingTestCase.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkLoadingTestCase.java @@ -1,138 +1,138 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you - * may not use this file except in compliance with the License. You may obtain a - * copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.eclipse.andmore.integration.tests; - -import static org.junit.Assert.*; - -import java.io.File; - -import com.android.ide.common.sdk.LoadStatus; - -import org.eclipse.andmore.AndmoreAndroidPlugin; -import org.eclipse.andmore.AndmoreAndroidPlugin.CheckSdkErrorHandler; -import org.eclipse.andmore.internal.preferences.AdtPrefs; -import org.eclipse.andmore.internal.sdk.AndroidTargetParser; -import org.eclipse.andmore.internal.sdk.Sdk; - -import com.android.sdklib.IAndroidTarget; - -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.junit.After; -import org.junit.BeforeClass; - -/** - * A test case which uses the SDK loaded by the ADT plugin. - */ -public abstract class SdkLoadingTestCase extends SdkTestCase { - - private Sdk mSdk; - - - /** - * Retrieve the {@link Sdk} under test. - */ - protected Sdk getSdk() { - System.out.println("getSdk"); - if (mSdk == null) { - mSdk = loadSdk(); - assertNotNull(mSdk); - validateSdk(mSdk); - } - return mSdk; - } - - /** - * Gets the current SDK from ADT, waiting if necessary. - */ - private Sdk loadSdk() { - AndmoreAndroidPlugin adt = AndmoreAndroidPlugin.getDefault(); - - // We'll never get an AdtPlugin object when running this with the - // non-Eclipse jUnit test runner. - if (adt == null) { - return null; - } - - // We'll never break out of the SDK load-wait-loop if the AdtPlugin - // doesn't - // actually have a valid SDK location because it won't have started an - // async load: - //String sdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); - String sdkLocation = System.getenv("ANDROID_HOME"); - if (sdkLocation == null || sdkLocation.length() == 0) { - sdkLocation = System.getenv("ADT_TEST_SDK_PATH"); - } - assertTrue("No valid SDK installation is set; for tests you typically need to set the" - + " environment variable ADT_TEST_SDK_PATH to point to an SDK folder", sdkLocation != null - && sdkLocation.length() > 0); - AdtPrefs.getPrefs().setSdkLocation(new File(sdkLocation)); - - Object sdkLock = Sdk.getLock(); - LoadStatus loadStatus = LoadStatus.LOADING; - // wait for ADT to load the SDK on a separate thread - // loop max of 600 times * 200 ms = 2 minutes - final int maxWait = 600; - for (int i = 0; i < maxWait && loadStatus == LoadStatus.LOADING; i++) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - // ignore - } - synchronized (sdkLock) { - loadStatus = adt.getSdkLoadStatus(); - } - } - Sdk sdk = null; - synchronized (sdkLock) { - assertEquals(LoadStatus.LOADED, loadStatus); - sdk = Sdk.getCurrent(); - } - - if (sdk.getTargets().length == 0) { - System.out.println("Did not find any valid targets. Reloading SDK from " + sdkLocation); - sdk = Sdk.loadSdk(sdkLocation); - } - assertNotNull(sdk); - return sdk; } - - protected boolean validateSdk(IAndroidTarget target) { - return true; - } - - /** - * Checks that the provided sdk contains one or more valid targets. - * - * @param sdk - * the {@link Sdk} to validate. - */ - @SuppressWarnings("unused") - private void validateSdk(Sdk sdk) { - assertTrue("sdk has no targets", sdk.getTargets().length > 0); - for (IAndroidTarget target : sdk.getTargets()) { - if (!validateSdk(target)) { - continue; - } - if (false) { // This takes forEVER - IStatus status = new AndroidTargetParser(target).run(new NullProgressMonitor()); - if (status.getCode() != IStatus.OK) { - fail("Failed to parse targets data"); - } - } - } - } -} +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.eclipse.andmore.integration.tests; + +import static org.junit.Assert.*; + +import java.io.File; + +import com.android.ide.common.sdk.LoadStatus; + +import org.eclipse.andmore.AndmoreAndroidPlugin; +import org.eclipse.andmore.AndmoreAndroidPlugin.CheckSdkErrorHandler; +import org.eclipse.andmore.internal.preferences.AdtPrefs; +import org.eclipse.andmore.internal.sdk.AndroidTargetParser; +import org.eclipse.andmore.internal.sdk.Sdk; + +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.junit.After; +import org.junit.BeforeClass; + +/** + * A test case which uses the SDK loaded by the ADT plugin. + */ +public abstract class SdkLoadingTestCase extends SdkTestCase { + + private Sdk mSdk; + + + /** + * Retrieve the {@link Sdk} under test. + */ + protected Sdk getSdk() { + System.out.println("getSdk"); + if (mSdk == null) { + mSdk = loadSdk(); + assertNotNull(mSdk); + validateSdk(mSdk); + } + return mSdk; + } + + /** + * Gets the current SDK from ADT, waiting if necessary. + */ + private Sdk loadSdk() { + AndmoreAndroidPlugin adt = AndmoreAndroidPlugin.getDefault(); + + // We'll never get an AdtPlugin object when running this with the + // non-Eclipse jUnit test runner. + if (adt == null) { + return null; + } + + // We'll never break out of the SDK load-wait-loop if the AdtPlugin + // doesn't + // actually have a valid SDK location because it won't have started an + // async load: + //String sdkLocation = AdtPrefs.getPrefs().getOsSdkFolder(); + String sdkLocation = System.getenv("ANDROID_HOME"); + if (sdkLocation == null || sdkLocation.length() == 0) { + sdkLocation = System.getenv("ADT_TEST_SDK_PATH"); + } + assertTrue("No valid SDK installation is set; for tests you typically need to set the" + + " environment variable ADT_TEST_SDK_PATH to point to an SDK folder", sdkLocation != null + && sdkLocation.length() > 0); + AdtPrefs.getPrefs().setSdkLocation(new File(sdkLocation)); + + Object sdkLock = Sdk.getLock(); + LoadStatus loadStatus = LoadStatus.LOADING; + // wait for ADT to load the SDK on a separate thread + // loop max of 600 times * 200 ms = 2 minutes + final int maxWait = 600; + for (int i = 0; i < maxWait && loadStatus == LoadStatus.LOADING; i++) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + // ignore + } + synchronized (sdkLock) { + loadStatus = adt.getSdkLoadStatus(); + } + } + Sdk sdk = null; + synchronized (sdkLock) { + assertEquals(LoadStatus.LOADED, loadStatus); + sdk = Sdk.getCurrent(); + } + + if (sdk.getTargets().size() == 0) { + System.out.println("Did not find any valid targets. Reloading SDK from " + sdkLocation); + sdk = Sdk.loadSdk(sdkLocation); + } + assertNotNull(sdk); + return sdk; } + + protected boolean validateSdk(IAndroidTarget target) { + return true; + } + + /** + * Checks that the provided sdk contains one or more valid targets. + * + * @param sdk + * the {@link Sdk} to validate. + */ + @SuppressWarnings("unused") + private void validateSdk(Sdk sdk) { + assertTrue("sdk has no targets", sdk.getTargets().size() > 0); + for (IAndroidTarget target : sdk.getTargets()) { + if (!validateSdk(target)) { + continue; + } + if (false) { // This takes forEVER + IStatus status = new AndroidTargetParser(target).run(new NullProgressMonitor()); + if (status.getCode() != IStatus.OK) { + fail("Failed to parse targets data"); + } + } + } + } +} diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkTestCase.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkTestCase.java index 808740ca..d74c7f66 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkTestCase.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/SdkTestCase.java @@ -1,423 +1,424 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.eclipse.andmore.integration.tests; - -import static org.eclipse.andmore.test.utils.XMLAssert.*; - -import org.custommonkey.xmlunit.*; -import org.eclipse.andmore.AndmoreAndroidPlugin; - -import com.android.SdkConstants; -import com.google.common.base.Charsets; -import com.google.common.collect.Sets; -import com.google.common.io.ByteStreams; -import com.google.common.io.Closeables; -import com.google.common.io.Files; -import com.google.common.io.InputSupplier; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.rules.TemporaryFolder; -import org.junit.rules.TestName; -import org.xml.sax.SAXException; - -/** - * Common test case for SDK unit tests. Contains a number of general utility - * methods to help writing test cases, such as looking up a temporary directory, - * comparing golden files, computing string diffs, etc. - */ -@SuppressWarnings("javadoc") - - -public abstract class SdkTestCase { - - @Rule - public TestName testName = new TestName(); - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - - /** Update golden files if different from the actual results */ - private static final boolean UPDATE_DIFFERENT_FILES = false; - /** Create golden files if missing */ - private static final boolean UPDATE_MISSING_FILES = true; - protected static List sCleanDirs; - - protected String getTestDataRelPath() { - fail("Must be overridden"); - return null; - } - - @BeforeClass - public static void setupClass() { - sCleanDirs = new CopyOnWriteArrayList(); - } - - @AfterClass - public synchronized static void tearDown() { - for (File file : sCleanDirs) { - deleteFile(file); - - sCleanDirs.remove(file); - } - } - - @After - public void tearDownProjects() throws Exception { - System.out.println("Test Completed: " + testName.getMethodName()); - } - - public static int getCaretOffset(String fileContent, String caretLocation) { - assertTrue(caretLocation, caretLocation.contains("^")); //$NON-NLS-1$ - int caretDelta = caretLocation.indexOf("^"); //$NON-NLS-1$ - assertTrue(caretLocation, caretDelta != -1); - // String around caret/range without the range and caret marker - // characters - String caretContext; - if (caretLocation.contains("[^")) { //$NON-NLS-1$ - caretDelta--; - assertTrue(caretLocation, caretLocation.startsWith("[^", caretDelta)); //$NON-NLS-1$ - int caretRangeEnd = caretLocation.indexOf(']', caretDelta + 2); - assertTrue(caretLocation, caretRangeEnd != -1); - caretContext = caretLocation.substring(0, caretDelta) - + caretLocation.substring(caretDelta + 2, caretRangeEnd) - + caretLocation.substring(caretRangeEnd + 1); - } else { - caretContext = caretLocation.substring(0, caretDelta) + caretLocation.substring(caretDelta + 1); // +1: - // skip - // "^" - } - int caretContextIndex = fileContent.indexOf(caretContext); - assertTrue("Caret content " + caretContext + " not found in file", caretContextIndex != -1); - return caretContextIndex + caretDelta; - } - - public static String addSelection(String newFileContents, int selectionBegin, int selectionEnd) { - // Insert selection markers -- [ ] for the selection range, ^ for the - // caret - String newFileWithCaret; - if (selectionBegin < selectionEnd) { - newFileWithCaret = newFileContents.substring(0, selectionBegin) + "[^" - + newFileContents.substring(selectionBegin, selectionEnd) + "]" - + newFileContents.substring(selectionEnd); - } else { - // Selected range - newFileWithCaret = newFileContents.substring(0, selectionBegin) + "^" - + newFileContents.substring(selectionBegin); - } - return newFileWithCaret; - } - - public static String getCaretContext(String file, int offset) { - int windowSize = 20; - int begin = Math.max(0, offset - windowSize / 2); - int end = Math.min(file.length(), offset + windowSize / 2); - return "..." + file.substring(begin, offset) + "^" + file.substring(offset, end) + "..."; - } - - /** Get the location to write missing golden files to */ - protected File getTargetDir() { - // Set $ADT_SDK_SOURCE_PATH to point to your git "sdk" directory; if - // done, then - // if you run a unit test which refers to a golden file which does not - // exist, it - // will be created directly into the test data directory and you can - // rerun the - // test - // and it should pass (after you verify that the golden file contains - // the correct - // result of course). - String sdk = System.getenv("ADT_SDK_SOURCE_PATH"); - if (sdk != null) { - File sdkPath = new File(sdk); - if (sdkPath.exists()) { - File testData = new File(sdkPath, getTestDataRelPath().replace('/', File.separatorChar)); - if (testData.exists()) { - addCleanupDir(testData); - return testData; - } - } - } - return getTempDir(); - } - - public File getTempDir() { - return temporaryFolder.getRoot(); - } - - protected String removeSessionData(String data) { - return data; - } - - protected InputStream getTestResource(String relativePath, boolean expectExists) { - String path = "testdata" + File.separator + relativePath; //$NON-NLS-1$ - InputStream stream = SdkTestCase.class.getResourceAsStream(path); - if (!expectExists && stream == null) { - return null; - } - return stream; - } - - @SuppressWarnings("resource") - protected String readTestFile(String relativePath, boolean expectExists) throws IOException { - InputStream stream = getTestResource(relativePath, expectExists); - if (expectExists) { - assertNotNull(relativePath + " does not exist", stream); - } else if (stream == null) { - return null; - } - String xml = new String(ByteStreams.toByteArray(stream), Charsets.UTF_8); - try { - Closeables.close(stream, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - assertTrue(xml.length() > 0); - // Remove any references to the project name such that we are isolated - // from - // that in golden file. - // Appears in strings.xml etc. - xml = removeSessionData(xml); - return xml; - } - - protected void assertEqualsGolden(String basename, String actual) throws IOException { - assertEqualsGolden(basename, actual, basename.substring(basename.lastIndexOf('.') + 1)); - } - - protected void assertEqualsGolden(String basename, String actual, String newExtension) throws IOException { - String testName = this.testName.getMethodName(); - if (testName.startsWith("test")) { - testName = testName.substring(4); - if (Character.isUpperCase(testName.charAt(0))) { - testName = Character.toLowerCase(testName.charAt(0)) + testName.substring(1); - } - } - String expectedName; - String extension = basename.substring(basename.lastIndexOf('.') + 1); - if (newExtension == null) { - newExtension = extension; - } - expectedName = basename.substring(0, basename.indexOf('.')) + "-expected-" + testName + '.' + newExtension; - String expected = readTestFile(expectedName, false); - if (expected == null) { - File expectedPath = new File(UPDATE_MISSING_FILES ? getTargetDir() : getTempDir(), expectedName); - Files.write(actual, expectedPath, Charsets.UTF_8); - System.out.println("Expected - written to " + expectedPath + ":\n"); - System.out.println(actual); - fail("Did not find golden file (" + expectedName + "): Wrote contents as " + expectedPath); - } else { - if (!expected.replaceAll("\r\n", "\n").equals(actual.replaceAll("\r\n", "\n"))) { - File expectedPath = new File(getTempDir(), expectedName); - File actualPath = new File(getTempDir(), expectedName.replace("expected", "actual")); - Files.write(expected, expectedPath, Charsets.UTF_8); - Files.write(actual, actualPath, Charsets.UTF_8); - // Also update data dir with the current value - if (UPDATE_DIFFERENT_FILES) { - Files.write(actual, new File(getTargetDir(), expectedName), Charsets.UTF_8); - } - System.out.println("The files differ: diff " + expectedPath + " " + actualPath); - Reader expectedReader = new InputStreamReader( new FileInputStream(expectedPath)); - Reader actualReader = new InputStreamReader(new FileInputStream(actualPath)); - - try { - XMLUnit.setIgnoreAttributeOrder(true); - XMLUnit.setIgnoreWhitespace(true); - assertXMLEqual(expectedReader, actualReader); - } catch (SAXException e) { - assertEquals("The files differ - see " + expectedPath + " versus " + actualPath, expected, actual); - } finally { - expectedReader.close(); - actualReader.close(); - } - } - } - } - - /** Creates a diff of two strings */ - public static String getDiff(String before, String after) { - return getDiff(before.split("\n"), after.split("\n")); - } - - public static String getDiff(String[] before, String[] after) { - // Based on the LCS section in - // http://introcs.cs.princeton.edu/java/96optimization/ - StringBuilder sb = new StringBuilder(); - int n = before.length; - int m = after.length; - // Compute longest common subsequence of x[i..m] and y[j..n] bottom up - int[][] lcs = new int[n + 1][m + 1]; - for (int i = n - 1; i >= 0; i--) { - for (int j = m - 1; j >= 0; j--) { - if (before[i].equals(after[j])) { - lcs[i][j] = lcs[i + 1][j + 1] + 1; - } else { - lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]); - } - } - } - int i = 0; - int j = 0; - while ((i < n) && (j < m)) { - if (before[i].equals(after[j])) { - i++; - j++; - } else { - sb.append("@@ -"); - sb.append(Integer.toString(i + 1)); - sb.append(" +"); - sb.append(Integer.toString(j + 1)); - sb.append('\n'); - while (i < n && j < m && !before[i].equals(after[j])) { - if (lcs[i + 1][j] >= lcs[i][j + 1]) { - sb.append('-'); - if (!before[i].trim().isEmpty()) { - sb.append(' '); - } - sb.append(before[i]); - sb.append('\n'); - i++; - } else { - sb.append('+'); - if (!after[j].trim().isEmpty()) { - sb.append(' '); - } - sb.append(after[j]); - sb.append('\n'); - j++; - } - } - } - } - if (i < n || j < m) { - assert i == n || j == m; - sb.append("@@ -"); - sb.append(Integer.toString(i + 1)); - sb.append(" +"); - sb.append(Integer.toString(j + 1)); - sb.append('\n'); - for (; i < n; i++) { - sb.append('-'); - if (!before[i].trim().isEmpty()) { - sb.append(' '); - } - sb.append(before[i]); - sb.append('\n'); - } - for (; j < m; j++) { - sb.append('+'); - if (!after[j].trim().isEmpty()) { - sb.append(' '); - } - sb.append(after[j]); - sb.append('\n'); - } - } - return sb.toString(); - } - - public static void deleteFile(File dir) { - if (dir.isDirectory()) { - for (File f : dir.listFiles()) { - deleteFile(f); - } - } else if (dir.isFile()) { - assertTrue(dir.getPath(), dir.delete()); - } - } - - protected File makeTestFile(String name, String relative, final InputStream contents) throws IOException { - return makeTestFile(getTargetDir(), name, relative, contents); - } - - protected File makeTestFile(File dir, String name, String relative, final InputStream contents) throws IOException { - if (relative != null) { - dir = new File(dir, relative); - if (!dir.exists()) { - boolean mkdir = dir.mkdirs(); - assertTrue(dir.getPath(), mkdir); - } - } else if (!dir.exists()) { - boolean mkdir = dir.mkdirs(); - assertTrue(dir.getPath(), mkdir); - } - File tempFile = new File(dir, name); - if (tempFile.exists()) { - tempFile.delete(); - } - Files.copy(new InputSupplier() { - @Override - public InputStream getInput() throws IOException { - return contents; - } - }, tempFile); - return tempFile; - } - - protected File getTestfile(File targetDir, String relativePath) throws IOException { - // Support replacing filenames and paths with a => syntax, e.g. - // dir/file.txt=>dir2/dir3/file2.java - // will read dir/file.txt from the test data and write it into the - // target - // directory as dir2/dir3/file2.java - String targetPath = relativePath; - int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$ - if (replaceIndex != -1) { - // foo=>bar - targetPath = relativePath.substring(replaceIndex + "=>".length()); - relativePath = relativePath.substring(0, replaceIndex); - } - InputStream stream = getTestResource(relativePath, true); - assertNotNull(relativePath + " does not exist", stream); - int index = targetPath.lastIndexOf('/'); - String relative = null; - String name = targetPath; - if (index != -1) { - name = targetPath.substring(index + 1); - relative = targetPath.substring(0, index); - } - return makeTestFile(targetDir, name, relative, stream); - } - - protected static void addCleanupDir(File dir) { - sCleanDirs.add(dir); - try { - sCleanDirs.add(dir.getCanonicalFile()); - } catch (IOException e) { - fail(e.getLocalizedMessage()); - } - sCleanDirs.add(dir.getAbsoluteFile()); - } - - +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.eclipse.andmore.integration.tests; + +import static org.eclipse.andmore.test.utils.XMLAssert.*; + +import org.custommonkey.xmlunit.*; +import org.eclipse.andmore.AndmoreAndroidPlugin; + +import com.android.SdkConstants; +import com.google.common.base.Charsets; +import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closeables; +import com.google.common.io.Files; +import com.google.common.io.ByteSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestName; +import org.xml.sax.SAXException; + +/** + * Common test case for SDK unit tests. Contains a number of general utility + * methods to help writing test cases, such as looking up a temporary directory, + * comparing golden files, computing string diffs, etc. + */ +@SuppressWarnings("javadoc") + + +public abstract class SdkTestCase { + + @Rule + public TestName testName = new TestName(); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + + /** Update golden files if different from the actual results */ + private static final boolean UPDATE_DIFFERENT_FILES = false; + /** Create golden files if missing */ + private static final boolean UPDATE_MISSING_FILES = true; + protected static List sCleanDirs; + + protected String getTestDataRelPath() { + fail("Must be overridden"); + return null; + } + + @BeforeClass + public static void setupClass() { + sCleanDirs = new CopyOnWriteArrayList(); + } + + @AfterClass + public synchronized static void tearDown() { + for (File file : sCleanDirs) { + deleteFile(file); + + sCleanDirs.remove(file); + } + } + + @After + public void tearDownProjects() throws Exception { + System.out.println("Test Completed: " + testName.getMethodName()); + } + + public static int getCaretOffset(String fileContent, String caretLocation) { + assertTrue(caretLocation, caretLocation.contains("^")); //$NON-NLS-1$ + int caretDelta = caretLocation.indexOf("^"); //$NON-NLS-1$ + assertTrue(caretLocation, caretDelta != -1); + // String around caret/range without the range and caret marker + // characters + String caretContext; + if (caretLocation.contains("[^")) { //$NON-NLS-1$ + caretDelta--; + assertTrue(caretLocation, caretLocation.startsWith("[^", caretDelta)); //$NON-NLS-1$ + int caretRangeEnd = caretLocation.indexOf(']', caretDelta + 2); + assertTrue(caretLocation, caretRangeEnd != -1); + caretContext = caretLocation.substring(0, caretDelta) + + caretLocation.substring(caretDelta + 2, caretRangeEnd) + + caretLocation.substring(caretRangeEnd + 1); + } else { + caretContext = caretLocation.substring(0, caretDelta) + caretLocation.substring(caretDelta + 1); // +1: + // skip + // "^" + } + int caretContextIndex = fileContent.indexOf(caretContext); + assertTrue("Caret content " + caretContext + " not found in file", caretContextIndex != -1); + return caretContextIndex + caretDelta; + } + + public static String addSelection(String newFileContents, int selectionBegin, int selectionEnd) { + // Insert selection markers -- [ ] for the selection range, ^ for the + // caret + String newFileWithCaret; + if (selectionBegin < selectionEnd) { + newFileWithCaret = newFileContents.substring(0, selectionBegin) + "[^" + + newFileContents.substring(selectionBegin, selectionEnd) + "]" + + newFileContents.substring(selectionEnd); + } else { + // Selected range + newFileWithCaret = newFileContents.substring(0, selectionBegin) + "^" + + newFileContents.substring(selectionBegin); + } + return newFileWithCaret; + } + + public static String getCaretContext(String file, int offset) { + int windowSize = 20; + int begin = Math.max(0, offset - windowSize / 2); + int end = Math.min(file.length(), offset + windowSize / 2); + return "..." + file.substring(begin, offset) + "^" + file.substring(offset, end) + "..."; + } + + /** Get the location to write missing golden files to */ + protected File getTargetDir() { + // Set $ADT_SDK_SOURCE_PATH to point to your git "sdk" directory; if + // done, then + // if you run a unit test which refers to a golden file which does not + // exist, it + // will be created directly into the test data directory and you can + // rerun the + // test + // and it should pass (after you verify that the golden file contains + // the correct + // result of course). + String sdk = System.getenv("ADT_SDK_SOURCE_PATH"); + if (sdk != null) { + File sdkPath = new File(sdk); + if (sdkPath.exists()) { + File testData = new File(sdkPath, getTestDataRelPath().replace('/', File.separatorChar)); + if (testData.exists()) { + addCleanupDir(testData); + return testData; + } + } + } + return getTempDir(); + } + + public File getTempDir() { + return temporaryFolder.getRoot(); + } + + protected String removeSessionData(String data) { + return data; + } + + protected InputStream getTestResource(String relativePath, boolean expectExists) { + String path = "testdata" + File.separator + relativePath; //$NON-NLS-1$ + InputStream stream = SdkTestCase.class.getResourceAsStream(path); + if (!expectExists && stream == null) { + return null; + } + return stream; + } + + @SuppressWarnings("resource") + protected String readTestFile(String relativePath, boolean expectExists) throws IOException { + InputStream stream = getTestResource(relativePath, expectExists); + if (expectExists) { + assertNotNull(relativePath + " does not exist", stream); + } else if (stream == null) { + return null; + } + String xml = new String(ByteStreams.toByteArray(stream), Charsets.UTF_8); + try { + Closeables.close(stream, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + assertTrue(xml.length() > 0); + // Remove any references to the project name such that we are isolated + // from + // that in golden file. + // Appears in strings.xml etc. + xml = removeSessionData(xml); + return xml; + } + + protected void assertEqualsGolden(String basename, String actual) throws IOException { + assertEqualsGolden(basename, actual, basename.substring(basename.lastIndexOf('.') + 1)); + } + + protected void assertEqualsGolden(String basename, String actual, String newExtension) throws IOException { + String testName = this.testName.getMethodName(); + if (testName.startsWith("test")) { + testName = testName.substring(4); + if (Character.isUpperCase(testName.charAt(0))) { + testName = Character.toLowerCase(testName.charAt(0)) + testName.substring(1); + } + } + String expectedName; + String extension = basename.substring(basename.lastIndexOf('.') + 1); + if (newExtension == null) { + newExtension = extension; + } + expectedName = basename.substring(0, basename.indexOf('.')) + "-expected-" + testName + '.' + newExtension; + String expected = readTestFile(expectedName, false); + if (expected == null) { + File expectedPath = new File(UPDATE_MISSING_FILES ? getTargetDir() : getTempDir(), expectedName); + Files.write(actual, expectedPath, Charsets.UTF_8); + System.out.println("Expected - written to " + expectedPath + ":\n"); + System.out.println(actual); + fail("Did not find golden file (" + expectedName + "): Wrote contents as " + expectedPath); + } else { + if (!expected.replaceAll("\r\n", "\n").equals(actual.replaceAll("\r\n", "\n"))) { + File expectedPath = new File(getTempDir(), expectedName); + File actualPath = new File(getTempDir(), expectedName.replace("expected", "actual")); + Files.write(expected, expectedPath, Charsets.UTF_8); + Files.write(actual, actualPath, Charsets.UTF_8); + // Also update data dir with the current value + if (UPDATE_DIFFERENT_FILES) { + Files.write(actual, new File(getTargetDir(), expectedName), Charsets.UTF_8); + } + System.out.println("The files differ: diff " + expectedPath + " " + actualPath); + Reader expectedReader = new InputStreamReader( new FileInputStream(expectedPath)); + Reader actualReader = new InputStreamReader(new FileInputStream(actualPath)); + + try { + XMLUnit.setIgnoreAttributeOrder(true); + XMLUnit.setIgnoreWhitespace(true); + assertXMLEqual(expectedReader, actualReader); + } catch (SAXException e) { + assertEquals("The files differ - see " + expectedPath + " versus " + actualPath, expected, actual); + } finally { + expectedReader.close(); + actualReader.close(); + } + } + } + } + + /** Creates a diff of two strings */ + public static String getDiff(String before, String after) { + return getDiff(before.split("\n"), after.split("\n")); + } + + public static String getDiff(String[] before, String[] after) { + // Based on the LCS section in + // http://introcs.cs.princeton.edu/java/96optimization/ + StringBuilder sb = new StringBuilder(); + int n = before.length; + int m = after.length; + // Compute longest common subsequence of x[i..m] and y[j..n] bottom up + int[][] lcs = new int[n + 1][m + 1]; + for (int i = n - 1; i >= 0; i--) { + for (int j = m - 1; j >= 0; j--) { + if (before[i].equals(after[j])) { + lcs[i][j] = lcs[i + 1][j + 1] + 1; + } else { + lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]); + } + } + } + int i = 0; + int j = 0; + while ((i < n) && (j < m)) { + if (before[i].equals(after[j])) { + i++; + j++; + } else { + sb.append("@@ -"); + sb.append(Integer.toString(i + 1)); + sb.append(" +"); + sb.append(Integer.toString(j + 1)); + sb.append('\n'); + while (i < n && j < m && !before[i].equals(after[j])) { + if (lcs[i + 1][j] >= lcs[i][j + 1]) { + sb.append('-'); + if (!before[i].trim().isEmpty()) { + sb.append(' '); + } + sb.append(before[i]); + sb.append('\n'); + i++; + } else { + sb.append('+'); + if (!after[j].trim().isEmpty()) { + sb.append(' '); + } + sb.append(after[j]); + sb.append('\n'); + j++; + } + } + } + } + if (i < n || j < m) { + assert i == n || j == m; + sb.append("@@ -"); + sb.append(Integer.toString(i + 1)); + sb.append(" +"); + sb.append(Integer.toString(j + 1)); + sb.append('\n'); + for (; i < n; i++) { + sb.append('-'); + if (!before[i].trim().isEmpty()) { + sb.append(' '); + } + sb.append(before[i]); + sb.append('\n'); + } + for (; j < m; j++) { + sb.append('+'); + if (!after[j].trim().isEmpty()) { + sb.append(' '); + } + sb.append(after[j]); + sb.append('\n'); + } + } + return sb.toString(); + } + + public static void deleteFile(File dir) { + if (dir.isDirectory()) { + for (File f : dir.listFiles()) { + deleteFile(f); + } + } else if (dir.isFile()) { + assertTrue(dir.getPath(), dir.delete()); + } + } + + protected File makeTestFile(String name, String relative, final InputStream contents) throws IOException { + return makeTestFile(getTargetDir(), name, relative, contents); + } + + protected File makeTestFile(File dir, String name, String relative, final InputStream contents) throws IOException { + if (relative != null) { + dir = new File(dir, relative); + if (!dir.exists()) { + boolean mkdir = dir.mkdirs(); + assertTrue(dir.getPath(), mkdir); + } + } else if (!dir.exists()) { + boolean mkdir = dir.mkdirs(); + assertTrue(dir.getPath(), mkdir); + } + File tempFile = new File(dir, name); + if (tempFile.exists()) { + tempFile.delete(); + } + ByteSource byteSource = new ByteSource() { + @Override + public InputStream openStream() throws IOException { + return contents; + } + }; + byteSource.copyTo(Files.asByteSink(tempFile)); + return tempFile; + } + + protected File getTestfile(File targetDir, String relativePath) throws IOException { + // Support replacing filenames and paths with a => syntax, e.g. + // dir/file.txt=>dir2/dir3/file2.java + // will read dir/file.txt from the test data and write it into the + // target + // directory as dir2/dir3/file2.java + String targetPath = relativePath; + int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$ + if (replaceIndex != -1) { + // foo=>bar + targetPath = relativePath.substring(replaceIndex + "=>".length()); + relativePath = relativePath.substring(0, replaceIndex); + } + InputStream stream = getTestResource(relativePath, true); + assertNotNull(relativePath + " does not exist", stream); + int index = targetPath.lastIndexOf('/'); + String relative = null; + String name = targetPath; + if (index != -1) { + name = targetPath.substring(index + 1); + relative = targetPath.substring(0, index); + } + return makeTestFile(targetDir, name, relative, stream); + } + + protected static void addCleanupDir(File dir) { + sCleanDirs.add(dir); + try { + sCleanDirs.add(dir.getCanonicalFile()); + } catch (IOException e) { + fail(e.getLocalizedMessage()); + } + sCleanDirs.add(dir.getAbsoluteFile()); + } + + } \ No newline at end of file diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/layoutRendering/ApiDemosRenderingTest.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/layoutRendering/ApiDemosRenderingTest.java index c08d8387..9a55f069 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/layoutRendering/ApiDemosRenderingTest.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/layoutRendering/ApiDemosRenderingTest.java @@ -1,333 +1,332 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you - * may not use this file except in compliance with the License. You may obtain a - * copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.eclipse.andmore.integration.tests.functests.layoutRendering; - -import static org.junit.Assert.*; - -import com.android.SdkConstants; -import com.android.ide.common.rendering.LayoutLibrary; -import com.android.ide.common.rendering.api.ActionBarCallback; -import com.android.ide.common.rendering.api.AdapterBinding; -import com.android.ide.common.rendering.api.HardwareConfig; -import com.android.ide.common.rendering.api.ILayoutPullParser; -import com.android.ide.common.rendering.api.IProjectCallback; -import com.android.ide.common.rendering.api.LayoutlibCallback; -import com.android.ide.common.rendering.api.RenderSession; -import com.android.ide.common.rendering.api.ResourceReference; -import com.android.ide.common.rendering.api.ResourceValue; -import com.android.ide.common.rendering.api.SessionParams; -import com.android.ide.common.rendering.api.SessionParams.RenderingMode; -import com.android.ide.common.resources.ResourceItem; -import com.android.ide.common.resources.ResourceRepository; -import com.android.ide.common.resources.ResourceResolver; -import com.android.ide.common.resources.configuration.DensityQualifier; -import com.android.ide.common.resources.configuration.FolderConfiguration; -import com.android.ide.common.resources.configuration.KeyboardStateQualifier; -import com.android.ide.common.resources.configuration.NavigationMethodQualifier; -import com.android.ide.common.resources.configuration.NavigationStateQualifier; -import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; -import com.android.ide.common.resources.configuration.ScreenHeightQualifier; -import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; -import com.android.ide.common.resources.configuration.ScreenRatioQualifier; -import com.android.ide.common.resources.configuration.ScreenSizeQualifier; -import com.android.ide.common.resources.configuration.ScreenWidthQualifier; -import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier; -import com.android.ide.common.resources.configuration.TextInputMethodQualifier; -import com.android.ide.common.resources.configuration.TouchScreenQualifier; -import com.android.ide.common.sdk.LoadStatus; - -import org.eclipse.andmore.integration.tests.SdkLoadingTestCase; -import org.eclipse.andmore.internal.resources.manager.ResourceManager; -import org.eclipse.andmore.internal.sdk.AndroidTargetData; - -import com.android.io.FolderWrapper; -import com.android.resources.Density; -import com.android.resources.Keyboard; -import com.android.resources.KeyboardState; -import com.android.resources.Navigation; -import com.android.resources.NavigationState; -import com.android.resources.ResourceType; -import com.android.resources.ScreenOrientation; -import com.android.resources.ScreenRatio; -import com.android.resources.ScreenRound; -import com.android.resources.ScreenSize; -import com.android.resources.TouchScreen; -import com.android.sdklib.IAndroidTarget; -import com.android.util.Pair; - -import org.junit.Ignore; -import org.junit.Test; -import org.kxml2.io.KXmlParser; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import javax.imageio.ImageIO; - -@Ignore -public class ApiDemosRenderingTest extends SdkLoadingTestCase { - - /** - * Custom parser that implements {@link ILayoutPullParser} (which itself - * extends {@link XmlPullParser}). - */ - private final static class TestParser extends KXmlParser implements ILayoutPullParser { - /** - * Since we're not going to go through the result of the - * rendering/layout, we can return null for the View Key. - */ - @Override - public Object getViewCookie() { - return null; - } - - @Override - public ILayoutPullParser getParser(String layoutName) { - return null; - } - } - - private final static class ProjectCallBack extends LayoutlibCallback { - // resource id counter. - // We start at 0x7f000000 to avoid colliding with the framework id - // since we have no access to the project R.java and we need to generate - // them automatically. - private int mIdCounter = 0x7f000000; - - // in some cases, the id that getResourceValue(String type, String name) - // returns - // will be sent back to get the type/name. This map stores the - // id/type/name we generate - // to be able to do the reverse resolution. - private Map> mResourceMap = new HashMap>(); - - private boolean mCustomViewAttempt = false; - - @Override - public String getNamespace() { - // TODO: read from the ApiDemos manifest. - return "com.example.android.apis"; - } - - @Override - @SuppressWarnings("unchecked") - public Object loadView(String name, Class[] constructorSignature, Object[] constructorArgs) - throws ClassNotFoundException, Exception { - mCustomViewAttempt = true; - return null; - } - - @Override - public Integer getResourceId(ResourceType type, String name) { - Integer result = ++mIdCounter; - mResourceMap.put(result, Pair.of(type, name)); - return result; - } - - @Override - public Pair resolveResourceId(int id) { - return mResourceMap.get(id); - } - - @Override - public String resolveResourceId(int[] id) { - return null; - } - - @Override - public ILayoutPullParser getParser(String layoutName) { - return null; - } - - @Override - public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie, - ResourceReference itemRef, int fullPosition, int typePosition, int fullChildPosition, - int typeChildPosition, ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) { - return null; - } - - @Override - public AdapterBinding getAdapterBinding(ResourceReference adapterView, Object adapterCookie, Object viewObject) { - return null; - } - - @Override - public ILayoutPullParser getParser(ResourceValue layoutResource) { - return null; - } - - @Override - public ActionBarCallback getActionBarCallback() { - return new ActionBarCallback(); - } - - @Override - public boolean supports(int ideFeature) { - // TODO Auto-generated method stub - return false; - } - } - - @Test - public void testApiDemos() throws IOException, XmlPullParserException { - findApiDemos(); - } - - private void findApiDemos() throws IOException, XmlPullParserException { - IAndroidTarget[] targets = getSdk().getTargets(); - - for (IAndroidTarget target : targets) { - String path = target.getPath(IAndroidTarget.SAMPLES); - File samples = new File(path); - if (samples.isDirectory()) { - File[] files = samples.listFiles(); - for (File file : files) { - if ("apidemos".equalsIgnoreCase(file.getName())) { - testSample(target, file); - return; - } - } - } - } - - fail("Failed to find ApiDemos!"); - } - - private void testSample(IAndroidTarget target, File sampleProject) throws IOException, XmlPullParserException { - AndroidTargetData data = getSdk().getTargetData(target); - if (data == null) { - fail("No AndroidData!"); - } - - LayoutLibrary layoutLib = data.getLayoutLibrary(); - if (layoutLib.getStatus() != LoadStatus.LOADED) { - fail("Fail to load the bridge: " + layoutLib.getLoadMessage()); - } - - FolderWrapper resFolder = new FolderWrapper(sampleProject, SdkConstants.FD_RES); - if (resFolder.exists() == false) { - fail("Sample project has no res folder!"); - } - - // look for the layout folder - File layoutFolder = new File(resFolder, SdkConstants.FD_RES_LAYOUT); - if (layoutFolder.isDirectory() == false) { - fail("Sample project has no layout folder!"); - } - - // first load the project's target framework resource - ResourceRepository framework = ResourceManager.getInstance().loadFrameworkResources(target); - - // now load the project resources - ResourceRepository project = new ResourceRepository(resFolder, false) { - @Override - protected ResourceItem createResourceItem(String name) { - return new ResourceItem(name); - } - - }; - - // Create a folder configuration that will be used for the rendering: - FolderConfiguration config = getConfiguration(); - - // get the configured resources - Map> configuredFramework = framework.getConfiguredResources(config); - Map> configuredProject = project.getConfiguredResources(config); - - boolean saveFiles = System.getenv("save_file") != null; - - // loop on the layouts and render them - File[] layouts = layoutFolder.listFiles(); - for (File layout : layouts) { - // create a parser for the layout file - TestParser parser = new TestParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(new FileReader(layout)); - - System.out.println("Rendering " + layout.getName()); - - ProjectCallBack projectCallBack = new ProjectCallBack(); - - ResourceResolver resolver = ResourceResolver - .create(configuredProject, configuredFramework, "Theme", false /* isProjectTheme */); - - HardwareConfig hardwareConfig = new HardwareConfig(320, 480, Density.MEDIUM, 160, // xdpi - 160, // ydpi - ScreenSize.NORMAL, ScreenOrientation.PORTRAIT, ScreenRound.NOTROUND, false /* - * software - * buttons - */); - - RenderSession session = layoutLib.createSession(new SessionParams(parser, RenderingMode.NORMAL, - null /* projectKey */, hardwareConfig, resolver, projectCallBack, 1, // minSdkVersion - 1, // targetSdkVersion - null // logger - )); - - if (session.getResult().isSuccess() == false) { - if (projectCallBack.mCustomViewAttempt == false) { - System.out.println("FAILED"); - fail(String.format("Rendering %1$s: %2$s", layout.getName(), session.getResult().getErrorMessage())); - } else { - System.out.println("Ignore custom views for now"); - } - } else { - if (saveFiles) { - File tmp = File.createTempFile(layout.getName(), ".png"); - ImageIO.write(session.getImage(), "png", tmp); - } - System.out.println("Success!"); - } - } - } - - /** - * Returns a config. This must be a valid config like a device would return. - * This is to prevent issues where some resources don't exist in all cases - * and not in the default (for instance only available in hdpi and mdpi but - * not in default). - * - * @return - */ - private FolderConfiguration getConfiguration() { - FolderConfiguration config = new FolderConfiguration(); - - // this matches an ADP1. - config.addQualifier(new SmallestScreenWidthQualifier(320)); - config.addQualifier(new ScreenWidthQualifier(320)); - config.addQualifier(new ScreenHeightQualifier(480)); - config.addQualifier(new ScreenSizeQualifier(ScreenSize.NORMAL)); - config.addQualifier(new ScreenRatioQualifier(ScreenRatio.NOTLONG)); - config.addQualifier(new ScreenOrientationQualifier(ScreenOrientation.PORTRAIT)); - config.addQualifier(new DensityQualifier(Density.MEDIUM)); - config.addQualifier(new TouchScreenQualifier(TouchScreen.FINGER)); - config.addQualifier(new KeyboardStateQualifier(KeyboardState.HIDDEN)); - config.addQualifier(new TextInputMethodQualifier(Keyboard.QWERTY)); - config.addQualifier(new NavigationStateQualifier(NavigationState.HIDDEN)); - config.addQualifier(new NavigationMethodQualifier(Navigation.TRACKBALL)); - config.addQualifier(new ScreenDimensionQualifier(480, 320)); - - config.updateScreenWidthAndHeight(); - - return config; - } -} +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.eclipse.andmore.integration.tests.functests.layoutRendering; + +import static org.junit.Assert.*; + +import com.android.SdkConstants; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.rendering.api.ActionBarCallback; +import com.android.ide.common.rendering.api.AdapterBinding; +import com.android.ide.common.rendering.api.HardwareConfig; +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.LayoutlibCallback; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceValueMap; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.resources.ResourceItem; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.KeyboardStateQualifier; +import com.android.ide.common.resources.configuration.NavigationMethodQualifier; +import com.android.ide.common.resources.configuration.NavigationStateQualifier; +import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; +import com.android.ide.common.resources.configuration.ScreenHeightQualifier; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.ScreenRatioQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.common.resources.configuration.ScreenWidthQualifier; +import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier; +import com.android.ide.common.resources.configuration.TextInputMethodQualifier; +import com.android.ide.common.resources.configuration.TouchScreenQualifier; +import com.android.ide.common.sdk.LoadStatus; + +import org.eclipse.andmore.integration.tests.SdkLoadingTestCase; +import org.eclipse.andmore.internal.resources.manager.ResourceManager; +import org.eclipse.andmore.internal.sdk.AndroidTargetData; + +import com.android.io.FolderWrapper; +import com.android.resources.Density; +import com.android.resources.Keyboard; +import com.android.resources.KeyboardState; +import com.android.resources.Navigation; +import com.android.resources.NavigationState; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenRatio; +import com.android.resources.ScreenRound; +import com.android.resources.ScreenSize; +import com.android.resources.TouchScreen; +import com.android.sdklib.IAndroidTarget; +import com.android.util.Pair; + +import org.junit.Ignore; +import org.junit.Test; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.imageio.ImageIO; + +@Ignore +public class ApiDemosRenderingTest extends SdkLoadingTestCase { + + /** + * Custom parser that implements {@link ILayoutPullParser} (which itself + * extends {@link XmlPullParser}). + */ + private final static class TestParser extends KXmlParser implements ILayoutPullParser { + /** + * Since we're not going to go through the result of the + * rendering/layout, we can return null for the View Key. + */ + @Override + public Object getViewCookie() { + return null; + } + + @Override + public ILayoutPullParser getParser(String layoutName) { + return null; + } + } + + private final static class ProjectCallBack extends LayoutlibCallback { + // resource id counter. + // We start at 0x7f000000 to avoid colliding with the framework id + // since we have no access to the project R.java and we need to generate + // them automatically. + private int mIdCounter = 0x7f000000; + + // in some cases, the id that getResourceValue(String type, String name) + // returns + // will be sent back to get the type/name. This map stores the + // id/type/name we generate + // to be able to do the reverse resolution. + private Map> mResourceMap = new HashMap>(); + + private boolean mCustomViewAttempt = false; + + @Override + public String getNamespace() { + // TODO: read from the ApiDemos manifest. + return "com.example.android.apis"; + } + + @Override + @SuppressWarnings("unchecked") + public Object loadView(String name, Class[] constructorSignature, Object[] constructorArgs) + throws ClassNotFoundException, Exception { + mCustomViewAttempt = true; + return null; + } + + @Override + public Integer getResourceId(ResourceType type, String name) { + Integer result = ++mIdCounter; + mResourceMap.put(result, Pair.of(type, name)); + return result; + } + + @Override + public Pair resolveResourceId(int id) { + return mResourceMap.get(id); + } + + @Override + public String resolveResourceId(int[] id) { + return null; + } + + @Override + public ILayoutPullParser getParser(String layoutName) { + return null; + } + + @Override + public Object getAdapterItemValue(ResourceReference adapterView, Object adapterCookie, + ResourceReference itemRef, int fullPosition, int typePosition, int fullChildPosition, + int typeChildPosition, ResourceReference viewRef, ViewAttribute viewAttribute, Object defaultValue) { + return null; + } + + @Override + public AdapterBinding getAdapterBinding(ResourceReference adapterView, Object adapterCookie, Object viewObject) { + return null; + } + + @Override + public ILayoutPullParser getParser(ResourceValue layoutResource) { + return null; + } + + @Override + public ActionBarCallback getActionBarCallback() { + return new ActionBarCallback(); + } + + @Override + public boolean supports(int ideFeature) { + // TODO Auto-generated method stub + return false; + } + } + + @Test + public void testApiDemos() throws IOException, XmlPullParserException { + findApiDemos(); + } + + private void findApiDemos() throws IOException, XmlPullParserException { + for (IAndroidTarget target : getSdk().getTargets()) { + String path = target.getPath(IAndroidTarget.SAMPLES); + File samples = new File(path); + if (samples.isDirectory()) { + File[] files = samples.listFiles(); + for (File file : files) { + if ("apidemos".equalsIgnoreCase(file.getName())) { + testSample(target, file); + return; + } + } + } + } + + fail("Failed to find ApiDemos!"); + } + + private void testSample(IAndroidTarget target, File sampleProject) throws IOException, XmlPullParserException { + AndroidTargetData data = getSdk().getTargetData(target); + if (data == null) { + fail("No AndroidData!"); + } + + LayoutLibrary layoutLib = data.getLayoutLibrary(); + if (layoutLib.getStatus() != LoadStatus.LOADED) { + fail("Fail to load the bridge: " + layoutLib.getLoadMessage()); + } + + FolderWrapper resFolder = new FolderWrapper(sampleProject, SdkConstants.FD_RES); + if (resFolder.exists() == false) { + fail("Sample project has no res folder!"); + } + + // look for the layout folder + File layoutFolder = new File(resFolder, SdkConstants.FD_RES_LAYOUT); + if (layoutFolder.isDirectory() == false) { + fail("Sample project has no layout folder!"); + } + + // first load the project's target framework resource + ResourceRepository framework = ResourceManager.getInstance().loadFrameworkResources(target); + + // now load the project resources + ResourceRepository project = new ResourceRepository(resFolder, false) { + @Override + protected ResourceItem createResourceItem(String name) { + return new ResourceItem(name); + } + + }; + + // Create a folder configuration that will be used for the rendering: + FolderConfiguration config = getConfiguration(); + + // get the configured resources + Map configuredFramework = framework.getConfiguredResources(config); + Map configuredProject = project.getConfiguredResources(config); + + boolean saveFiles = System.getenv("save_file") != null; + + // loop on the layouts and render them + File[] layouts = layoutFolder.listFiles(); + for (File layout : layouts) { + // create a parser for the layout file + TestParser parser = new TestParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(layout)); + + System.out.println("Rendering " + layout.getName()); + + ProjectCallBack projectCallBack = new ProjectCallBack(); + + ResourceResolver resolver = ResourceResolver + .create(configuredProject, configuredFramework, "Theme", false /* isProjectTheme */); + + HardwareConfig hardwareConfig = new HardwareConfig(320, 480, Density.MEDIUM, 160, // xdpi + 160, // ydpi + ScreenSize.NORMAL, ScreenOrientation.PORTRAIT, ScreenRound.NOTROUND, false /* + * software + * buttons + */); + + RenderSession session = layoutLib.createSession(new SessionParams(parser, RenderingMode.NORMAL, + null /* projectKey */, hardwareConfig, resolver, projectCallBack, 1, // minSdkVersion + 1, // targetSdkVersion + null // logger + )); + + if (session.getResult().isSuccess() == false) { + if (projectCallBack.mCustomViewAttempt == false) { + System.out.println("FAILED"); + fail(String.format("Rendering %1$s: %2$s", layout.getName(), session.getResult().getErrorMessage())); + } else { + System.out.println("Ignore custom views for now"); + } + } else { + if (saveFiles) { + File tmp = File.createTempFile(layout.getName(), ".png"); + ImageIO.write(session.getImage(), "png", tmp); + } + System.out.println("Success!"); + } + } + } + + /** + * Returns a config. This must be a valid config like a device would return. + * This is to prevent issues where some resources don't exist in all cases + * and not in the default (for instance only available in hdpi and mdpi but + * not in default). + * + * @return + */ + private FolderConfiguration getConfiguration() { + FolderConfiguration config = new FolderConfiguration(); + + // this matches an ADP1. + config.addQualifier(new SmallestScreenWidthQualifier(320)); + config.addQualifier(new ScreenWidthQualifier(320)); + config.addQualifier(new ScreenHeightQualifier(480)); + config.addQualifier(new ScreenSizeQualifier(ScreenSize.NORMAL)); + config.addQualifier(new ScreenRatioQualifier(ScreenRatio.NOTLONG)); + config.addQualifier(new ScreenOrientationQualifier(ScreenOrientation.PORTRAIT)); + config.addQualifier(new DensityQualifier(Density.MEDIUM)); + config.addQualifier(new TouchScreenQualifier(TouchScreen.FINGER)); + config.addQualifier(new KeyboardStateQualifier(KeyboardState.HIDDEN)); + config.addQualifier(new TextInputMethodQualifier(Keyboard.QWERTY)); + config.addQualifier(new NavigationStateQualifier(NavigationState.HIDDEN)); + config.addQualifier(new NavigationMethodQualifier(Navigation.TRACKBALL)); + config.addQualifier(new ScreenDimensionQualifier(480, 320)); + + config.updateScreenWidthAndHeight(); + + return config; + } +} diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/sampleProjects/SampleProjectTest.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/sampleProjects/SampleProjectTest.java index 1e18a2fa..042cd1ef 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/sampleProjects/SampleProjectTest.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/integration/tests/functests/sampleProjects/SampleProjectTest.java @@ -1,267 +1,266 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you - * may not use this file except in compliance with the License. You may obtain a - * copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.eclipse.andmore.integration.tests.functests.sampleProjects; - -import static org.junit.Assert.*; - -import com.android.SdkConstants; - -import org.eclipse.andmore.AdtUtils; -import org.eclipse.andmore.integration.tests.SdkLoadingTestCase; -import org.eclipse.andmore.internal.wizards.newproject.NewProjectCreator; -import org.eclipse.andmore.internal.wizards.newproject.NewProjectWizardState; -import org.eclipse.andmore.internal.wizards.newproject.NewProjectWizardState.Mode; - -import com.android.sdklib.IAndroidTarget; - -import org.eclipse.core.resources.IMarker; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IResourceChangeEvent; -import org.eclipse.core.resources.IResourceChangeListener; -import org.eclipse.core.resources.IResourceDelta; -import org.eclipse.core.resources.IResourceDeltaVisitor; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.jface.operation.IRunnableContext; -import org.eclipse.jface.operation.IRunnableWithProgress; -import org.eclipse.swt.widgets.Display; -import org.junit.Ignore; -import org.junit.Test; - -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Test case that verifies all SDK sample projects can be imported, and built in - * Eclipse. - *

- * TODO: add support for deploying apps onto emulator and verifying successful - * execution there - * - */ -@Ignore -public class SampleProjectTest extends SdkLoadingTestCase { - - private static final Logger sLogger = Logger.getLogger(SampleProjectTest.class.getName()); - - /** - * Finds all samples projects in set SDK and verify they can be built in - * Eclipse. - *

- * TODO: add install and run on emulator test - * - * @throws CoreException - */ - @Test - public void testSamples() throws CoreException { - // TODO: For reporting purposes, it would be better if a separate test - // success or failure - // could be reported for each sample - IAndroidTarget[] targets = getSdk().getTargets(); - for (IAndroidTarget target : targets) { - doTestSamplesForTarget(target); - } - } - - private void doTestSamplesForTarget(IAndroidTarget target) throws CoreException { - String path = target.getPath(IAndroidTarget.SAMPLES); - File samples = new File(path); - if (samples.isDirectory()) { - File[] files = samples.listFiles(); - for (File file : files) { - if (file.isDirectory()) { - doTestSampleProject(file.getName(), file.getAbsolutePath(), target); - } - } - } - } - - /** - * Tests the sample project with the given name - * - * @param target - * - SDK target of project - * @param name - * - name of sample project to test - * @param path - * - absolute file system path - * @throws CoreException - */ - private void doTestSampleProject(String name, String path, IAndroidTarget target) throws CoreException { - IProject iproject = null; - try { - sLogger.log(Level.INFO, String.format("Testing sample %s for target %s", name, target.getName())); - - prepareProject(path, target); - - IRunnableContext context = new IRunnableContext() { - @Override - public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) - throws InvocationTargetException, InterruptedException { - runnable.run(new NullProgressMonitor()); - } - }; - NewProjectWizardState state = new NewProjectWizardState(Mode.SAMPLE); - state.projectName = name; - state.target = target; - state.packageName = "com.android.samples"; - state.activityName = name; - state.applicationName = name; - state.chosenSample = new File(path); - state.useDefaultLocation = false; - state.createActivity = false; - - NewProjectCreator creator = new NewProjectCreator(state, context); - creator.createAndroidProjects(); - iproject = validateProjectExists(name); - validateNoProblems(iproject); - } catch (CoreException e) { - sLogger.log(Level.SEVERE, String.format("Unexpected exception when creating sample project %s " - + "for target %s", name, target.getName())); - throw e; - } finally { - if (iproject != null) { - iproject.delete(false, true, new NullProgressMonitor()); - } - } - } - - private void prepareProject(String path, IAndroidTarget target) { - if (target.getVersion().isPreview()) { - // need to explicitly set preview's version in manifest for project - // to compile - final String manifestPath = path + File.separatorChar + SdkConstants.FN_ANDROID_MANIFEST_XML; - AndroidManifestWriter manifestWriter = AndroidManifestWriter.parse(manifestPath); - assertNotNull(String.format("could not read manifest %s", manifestPath), manifestWriter); - assertTrue(manifestWriter.setMinSdkVersion(target.getVersion().getApiString())); - } - } - - private IProject validateProjectExists(String name) { - IProject iproject = getIProject(name); - assertTrue(String.format("%s project not created", name), iproject.exists()); - assertTrue(String.format("%s project not opened", name), iproject.isOpen()); - return iproject; - } - - private IProject getIProject(String name) { - IProject iproject = ResourcesPlugin.getWorkspace().getRoot().getProject(name); - return iproject; - } - - private void validateNoProblems(IProject iproject) throws CoreException { - waitForBuild(iproject); - - boolean hasErrors = false; - StringBuilder failureBuilder = new StringBuilder(String.format("%s project has errors:", iproject.getName())); - IMarker[] markers = iproject.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); - if (markers != null && markers.length > 0) { - // the project has marker(s). even though they are "problem" we - // don't know their severity. so we loop on them and figure if they - // are warnings or errors - for (IMarker m : markers) { - int s = m.getAttribute(IMarker.SEVERITY, -1); - if (s == IMarker.SEVERITY_ERROR) { - hasErrors = true; - failureBuilder.append("\n"); - failureBuilder.append(m.getAttribute(IMarker.MESSAGE, "")); - } - } - } - failureBuilder.append("Project location: " + AdtUtils.getAbsolutePath(iproject)); - assertFalse(failureBuilder.toString(), hasErrors); - } - - /** - * Waits for build to complete. - * - * @param iproject - */ - private void waitForBuild(final IProject iproject) { - - final BuiltProjectDeltaVisitor deltaVisitor = new BuiltProjectDeltaVisitor(iproject); - IResourceChangeListener newBuildListener = new IResourceChangeListener() { - - @Override - public void resourceChanged(IResourceChangeEvent event) { - try { - event.getDelta().accept(deltaVisitor); - } catch (CoreException e) { - fail(); - } - } - - }; - iproject.getWorkspace().addResourceChangeListener(newBuildListener, IResourceChangeEvent.POST_BUILD); - - // poll build listener to determine when build is done - // loop max of 1200 times * 50 ms = 60 seconds - final int maxWait = 1200; - for (int i = 0; i < maxWait; i++) { - if (deltaVisitor.isProjectBuilt()) { - return; - } - try { - Thread.sleep(50); - } catch (InterruptedException e) { - // ignore - } - if (Display.getCurrent() != null) { - Display.getCurrent().readAndDispatch(); - } - } - - sLogger.log(Level.SEVERE, "expected build event never happened?"); - fail(String.format("Expected build event never happened for %s", iproject.getName())); - } - - /** - * Scans a given IResourceDelta looking for a "build event" change for given - * IProject - * - */ - private class BuiltProjectDeltaVisitor implements IResourceDeltaVisitor { - - private IProject mIProject; - private boolean mIsBuilt; - - public BuiltProjectDeltaVisitor(IProject iproject) { - mIProject = iproject; - mIsBuilt = false; - } - - @Override - public boolean visit(IResourceDelta delta) { - if (mIProject.equals(delta.getResource())) { - setBuilt(true); - return false; - } - return true; - } - - private synchronized void setBuilt(boolean b) { - mIsBuilt = b; - } - - public synchronized boolean isProjectBuilt() { - return mIsBuilt; - } - } -} +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you + * may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.eclipse.andmore.integration.tests.functests.sampleProjects; + +import static org.junit.Assert.*; + +import com.android.SdkConstants; + +import org.eclipse.andmore.AdtUtils; +import org.eclipse.andmore.integration.tests.SdkLoadingTestCase; +import org.eclipse.andmore.internal.wizards.newproject.NewProjectCreator; +import org.eclipse.andmore.internal.wizards.newproject.NewProjectWizardState; +import org.eclipse.andmore.internal.wizards.newproject.NewProjectWizardState.Mode; + +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.swt.widgets.Display; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Test case that verifies all SDK sample projects can be imported, and built in + * Eclipse. + *

+ * TODO: add support for deploying apps onto emulator and verifying successful + * execution there + * + */ +@Ignore +public class SampleProjectTest extends SdkLoadingTestCase { + + private static final Logger sLogger = Logger.getLogger(SampleProjectTest.class.getName()); + + /** + * Finds all samples projects in set SDK and verify they can be built in + * Eclipse. + *

+ * TODO: add install and run on emulator test + * + * @throws CoreException + */ + @Test + public void testSamples() throws CoreException { + // TODO: For reporting purposes, it would be better if a separate test + // success or failure + // could be reported for each sample + for (IAndroidTarget target : getSdk().getTargets()) { + doTestSamplesForTarget(target); + } + } + + private void doTestSamplesForTarget(IAndroidTarget target) throws CoreException { + String path = target.getPath(IAndroidTarget.SAMPLES); + File samples = new File(path); + if (samples.isDirectory()) { + File[] files = samples.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + doTestSampleProject(file.getName(), file.getAbsolutePath(), target); + } + } + } + } + + /** + * Tests the sample project with the given name + * + * @param target + * - SDK target of project + * @param name + * - name of sample project to test + * @param path + * - absolute file system path + * @throws CoreException + */ + private void doTestSampleProject(String name, String path, IAndroidTarget target) throws CoreException { + IProject iproject = null; + try { + sLogger.log(Level.INFO, String.format("Testing sample %s for target %s", name, target.getName())); + + prepareProject(path, target); + + IRunnableContext context = new IRunnableContext() { + @Override + public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) + throws InvocationTargetException, InterruptedException { + runnable.run(new NullProgressMonitor()); + } + }; + NewProjectWizardState state = new NewProjectWizardState(Mode.SAMPLE); + state.projectName = name; + state.target = target; + state.packageName = "com.android.samples"; + state.activityName = name; + state.applicationName = name; + state.chosenSample = new File(path); + state.useDefaultLocation = false; + state.createActivity = false; + + NewProjectCreator creator = new NewProjectCreator(state, context); + creator.createAndroidProjects(); + iproject = validateProjectExists(name); + validateNoProblems(iproject); + } catch (CoreException e) { + sLogger.log(Level.SEVERE, String.format("Unexpected exception when creating sample project %s " + + "for target %s", name, target.getName())); + throw e; + } finally { + if (iproject != null) { + iproject.delete(false, true, new NullProgressMonitor()); + } + } + } + + private void prepareProject(String path, IAndroidTarget target) { + if (target.getVersion().isPreview()) { + // need to explicitly set preview's version in manifest for project + // to compile + final String manifestPath = path + File.separatorChar + SdkConstants.FN_ANDROID_MANIFEST_XML; + AndroidManifestWriter manifestWriter = AndroidManifestWriter.parse(manifestPath); + assertNotNull(String.format("could not read manifest %s", manifestPath), manifestWriter); + assertTrue(manifestWriter.setMinSdkVersion(target.getVersion().getApiString())); + } + } + + private IProject validateProjectExists(String name) { + IProject iproject = getIProject(name); + assertTrue(String.format("%s project not created", name), iproject.exists()); + assertTrue(String.format("%s project not opened", name), iproject.isOpen()); + return iproject; + } + + private IProject getIProject(String name) { + IProject iproject = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + return iproject; + } + + private void validateNoProblems(IProject iproject) throws CoreException { + waitForBuild(iproject); + + boolean hasErrors = false; + StringBuilder failureBuilder = new StringBuilder(String.format("%s project has errors:", iproject.getName())); + IMarker[] markers = iproject.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE); + if (markers != null && markers.length > 0) { + // the project has marker(s). even though they are "problem" we + // don't know their severity. so we loop on them and figure if they + // are warnings or errors + for (IMarker m : markers) { + int s = m.getAttribute(IMarker.SEVERITY, -1); + if (s == IMarker.SEVERITY_ERROR) { + hasErrors = true; + failureBuilder.append("\n"); + failureBuilder.append(m.getAttribute(IMarker.MESSAGE, "")); + } + } + } + failureBuilder.append("Project location: " + AdtUtils.getAbsolutePath(iproject)); + assertFalse(failureBuilder.toString(), hasErrors); + } + + /** + * Waits for build to complete. + * + * @param iproject + */ + private void waitForBuild(final IProject iproject) { + + final BuiltProjectDeltaVisitor deltaVisitor = new BuiltProjectDeltaVisitor(iproject); + IResourceChangeListener newBuildListener = new IResourceChangeListener() { + + @Override + public void resourceChanged(IResourceChangeEvent event) { + try { + event.getDelta().accept(deltaVisitor); + } catch (CoreException e) { + fail(); + } + } + + }; + iproject.getWorkspace().addResourceChangeListener(newBuildListener, IResourceChangeEvent.POST_BUILD); + + // poll build listener to determine when build is done + // loop max of 1200 times * 50 ms = 60 seconds + final int maxWait = 1200; + for (int i = 0; i < maxWait; i++) { + if (deltaVisitor.isProjectBuilt()) { + return; + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // ignore + } + if (Display.getCurrent() != null) { + Display.getCurrent().readAndDispatch(); + } + } + + sLogger.log(Level.SEVERE, "expected build event never happened?"); + fail(String.format("Expected build event never happened for %s", iproject.getName())); + } + + /** + * Scans a given IResourceDelta looking for a "build event" change for given + * IProject + * + */ + private class BuiltProjectDeltaVisitor implements IResourceDeltaVisitor { + + private IProject mIProject; + private boolean mIsBuilt; + + public BuiltProjectDeltaVisitor(IProject iproject) { + mIProject = iproject; + mIsBuilt = false; + } + + @Override + public boolean visit(IResourceDelta delta) { + if (mIProject.equals(delta.getResource())) { + setBuilt(true); + return false; + } + return true; + } + + private synchronized void setBuilt(boolean b) { + mIsBuilt = b; + } + + public synchronized boolean isProjectBuilt() { + return mIsBuilt; + } + } +} diff --git a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/internal/build/AaptParserTest.java b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/internal/build/AaptParserTest.java index 7361c690..28e04333 100644 --- a/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/internal/build/AaptParserTest.java +++ b/android-core/plugins/org.eclipse.andmore.integration.tests/src/org/eclipse/andmore/internal/build/AaptParserTest.java @@ -1,212 +1,212 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.eclipse.andmore.internal.build; - -import static org.junit.Assert.*; - -import org.eclipse.andmore.AndmoreAndroidConstants; -import org.eclipse.andmore.AndmoreAndroidPlugin; -import org.eclipse.andmore.internal.build.AaptParser; -import org.eclipse.andmore.internal.editors.layout.refactoring.AdtProjectTest; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IMarker; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.junit.Ignore; -import org.junit.Test; - -import java.io.File; -import java.util.Collections; -import java.util.List; - -public class AaptParserTest extends AdtProjectTest { - - @Test - public void testBasic() throws Exception { - // Test the "at 'property' with value 'value' range matching included - // with most aapt errors - checkRanges("quickfix1.xml", "res/layout/quickfix1.xml", - "quickfix1.xml:7: error: Error: No resource found that matches the given name (at" - + " 'text' with value '@string/firststring').", "android:text=\"^@string/firststring\"", - "android:text=\"@string/firststring^\""); - } - - @Test - public void testRange1() throws Exception { - // Check that when the actual aapt error occurs on a line later than the - // original error - // line, the forward search which looks for a value match does not stop - // on an - // earlier line that happens to have the same value prefix - checkRanges("aapterror1.xml", "res/layout/aapterror1.xml", - "aapterror1.xml:5: error: Error: Integer types not allowed (at " - + "'layout_marginBottom' with value '50').", "marginBottom=\"^50\"", "marginBottom=\"50^\""); - } - - @Test - public void testRange2() throws Exception { - // Check that when we have a duplicate resource error, we highlight both - // the original - // property and the original definition. - // This tests the second, duplicate declaration ration. - checkRanges("aapterror2.xml", "res/values/aapterror2.xml", - "aapterror2.xml:7: error: Resource entry repeatedStyle1 already has bag item " + "android:gravity.", - "bottom", "bottom"); - } - - @Test - @Ignore - public void testRange3() throws Exception { - // Check that when we have a duplicate resource error, we highlight both - // the original - // property and the original definition. - // This tests the original definition. Note that we don't have enough - // position info - // so we simply highlight the whitespace portion of the line. - checkRanges("aapterror2.xml", "res/values/aapterror2.xml", "aapterror2.xml:4: Originally defined here.", - "^left", "left^"); - } - - @Test - public void testRange4() throws Exception { - // Check for aapt error which occurs when the attribute name in an item - // style declaration - // is nonexistent - checkRanges("aapterror3.xml", "res/values/aapterror3.xml", - "aapterror3.xml:4: error: Error: No resource found that matches the given name: " - + "attr 'nonexistent'.", "5", - "5"); - } - - @Test - public void testRange5() throws Exception { - // Test missing resource name - checkRanges("aapterror4.xml", "res/values/aapterror4.xml", - "aapterror4.xml:3: error: A 'name' attribute is required for