package com.vpedak.testsrecorder.plugin.actions;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.BuildFileStatement;
import com.android.tools.idea.gradle.parser.Dependency;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.intellij.execution.ExecutionManager;
import com.intellij.execution.RunManager;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.facet.FacetManager;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.notification.*;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile;
import com.intellij.ui.components.panels.VerticalLayout;
import com.vpedak.testsrecorder.plugin.core.EventReader;
import com.vpedak.testsrecorder.plugin.core.Templates;
import com.vpedak.testsrecorder.plugin.ui.ActivitiesComboBoxModel;
import com.vpedak.testsrecorder.plugin.ui.EventsList;
import com.vpedak.testsrecorder.plugin.ui.ModulesComboBoxModel;
import com.vpedak.testsrecorder.plugin.ui.PluginConfiguration;
import org.jetbrains.android.dom.manifest.Activity;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class ToolsTestsRecorderAction extends com.intellij.openapi.actionSystem.AnAction implements TestListener {
public static final String TOOL_WINDOW_ID = "vpedak.tests.recorder,id";
public static final String TEST_FILE_NAME = "AndrTestRec.java";
public static final String ANDR_TEST_CLASSNAME = "AndrTestRec";
public static final String RUN_CONFIG_NAME = "TestRecorderTemporary";
public static final String RECORD = "Record";
public static final String STOP = "Stop";
public static final String TOOLWINDOW_TITLE = "Android Tests Recorder";
public static final String GRADLE_BUILD_SAVED = "gradle.build.saved";
private ModulesComboBoxModel moduleBoxModel;
private ActivitiesComboBoxModel activitiesBoxModel;
private ComboBox activitiesList;
private JButton recButton;
private EventsList eventsList;
private ExecutionChecker executionChecker;
private volatile ToolWindow toolWindow;
private String jarPath;
private Project project;
private VirtualFile testVirtualFile;
private static String template;
private EventReader eventReader;
private JLabel label;
private SimpleToolWindowPanel panel;
private long uniqueId;
private Module currentModule;
public ToolsTestsRecorderAction() {
super("Android Tests _Recorder");
this.jarPath = getJarPath();
if (template == null) {
template = Templates.getInstance().getTemplate("start_record");
}
eventsList = new EventsList();
eventReader = new EventReader(eventsList);
}
public void actionPerformed(final AnActionEvent event) {
notifyIfNecessary();
Project project = (Project) event.getData(PlatformDataKeys.PROJECT);
ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
this.toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID);
if (this.toolWindow == null) {
this.toolWindow = toolWindowManager.registerToolWindow(TOOL_WINDOW_ID, true, com.intellij.openapi.wm.ToolWindowAnchor.RIGHT, false);
this.toolWindow.setTitle(TOOLWINDOW_TITLE);
this.toolWindow.setStripeTitle(TOOLWINDOW_TITLE);
this.toolWindow.setIcon(IconLoader.getIcon("icons/main.png"));
this.toolWindow.setAutoHide(false);
panel = new SimpleToolWindowPanel(true);
final JToolBar toolBar = new JToolBar();
this.recButton = new JButton(RECORD, IconLoader.getIcon("icons/rec.png"));
this.recButton.addActionListener(new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if (ToolsTestsRecorderAction.this.recButton.getText().equals(RECORD)) {
ToolsTestsRecorderAction.this.recButton.setText(STOP);
ToolsTestsRecorderAction.this.recButton.setIcon(IconLoader.getIcon("icons/stop.png"));
ToolsTestsRecorderAction.this.record(event);
panel.remove(label);
panel.add(eventsList);
panel.repaint();
} else {
ToolsTestsRecorderAction.this.recButton.setText(RECORD);
ToolsTestsRecorderAction.this.recButton.setIcon(IconLoader.getIcon("icons/rec.png"));
ToolsTestsRecorderAction.this.stop(event);
}
}
});
toolBar.add(this.recButton);
ModuleManager moduleManager = ModuleManager.getInstance(project);
Module[] modules = moduleManager.getModules();
Module module = null;
VirtualFile virtualFile = (VirtualFile) event.getData(PlatformDataKeys.VIRTUAL_FILE);
if (virtualFile != null) {
module = com.intellij.openapi.roots.ProjectRootManager.getInstance(project).getFileIndex().getModuleForFile(virtualFile);
}
this.recButton.setEnabled(false);
this.moduleBoxModel = new ModulesComboBoxModel(modules, module);
ComboBox modList = new ComboBox(this.moduleBoxModel);
modList.addActionListener(new AbstractAction() {
public void actionPerformed(ActionEvent e) {
ToolsTestsRecorderAction.this.fillActivities((ModulesComboBoxModel.ModuleWrapper) ToolsTestsRecorderAction.this.moduleBoxModel.getSelected());
}
});
if (moduleBoxModel.getSelected() != null) {
modList.setPrototypeDisplayValue(moduleBoxModel.getSelected());
}
JLabel modLabel = new JLabel("Module: ", SwingConstants.RIGHT);
Border border = modLabel.getBorder();
Border margin = new EmptyBorder(0, 15, 0, 5);
modLabel.setBorder(new CompoundBorder(border, margin));
toolBar.add(modLabel);
toolBar.add(modList);
this.activitiesBoxModel = new ActivitiesComboBoxModel(Collections.<Activity>emptyList(), null);
this.activitiesList = new ComboBox(this.activitiesBoxModel);
this.activitiesList.addActionListener(new AbstractAction() {
public void actionPerformed(ActionEvent e) {
ToolsTestsRecorderAction.this.recButton.setEnabled(ToolsTestsRecorderAction.this.activitiesBoxModel.getSelected() != null);
}
});
final JLabel activityLabel = new JLabel("Activity: ", SwingConstants.RIGHT);
border = activityLabel.getBorder();
margin = new EmptyBorder(0, 15, 0, 5);
activityLabel.setBorder(new CompoundBorder(border, margin));
toolBar.add(activityLabel);
toolBar.add(this.activitiesList);
fillActivities(module == null ? null : new ModulesComboBoxModel.ModuleWrapper(module));
JButton helpButton = new JButton(IconLoader.getIcon("icons/help.png"));
helpButton.setToolTipText("Help");
helpButton.addActionListener(new AbstractAction() {
public void actionPerformed(ActionEvent e) {
openHelpWindow();
}
});
toolBar.add(helpButton);
KeyboardFocusManager keyManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
keyManager.addKeyEventDispatcher(new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == 112) { // F1
if (toolWindow.isActive()) {
openHelpWindow();
return true;
}
}
return false;
}
});
panel.setToolbar(toolBar);
label = new JLabel("Select Module and Activity to start recording.");
label.setHorizontalAlignment(JLabel.CENTER);
panel.add(label);
com.intellij.ui.content.Content toolContent = this.toolWindow.getContentManager().getFactory().createContent(panel, "", false);
this.toolWindow.getContentManager().addContent(toolContent);
new CheckNewVersionThread(this).start();
}
this.toolWindow.activate(null, true, true);
}
private void notifyIfNecessary() {
int numberOfRun = PropertiesComponent.getInstance().getOrInitInt(TOOL_WINDOW_ID, 1);
if (numberOfRun < 25) {
numberOfRun++;
PropertiesComponent.getInstance().setValue(TOOL_WINDOW_ID, String.valueOf(numberOfRun));
if (numberOfRun % 5 == 0) {
Notifications.Bus.notify(new Notification(TOOL_WINDOW_ID, "Do you like Android Test Recorder?",
"Please <a href='http://droidtestlab.com/share.html'>share your experience with your friends</a> to give me the opportunity to make it better.",
NotificationType.INFORMATION,
new
NotificationListener() {
@Override
public void hyperlinkUpdate(Notification notification, HyperlinkEvent hyperlinkEvent) {
PropertiesComponent.getInstance().setValue(TOOL_WINDOW_ID, String.valueOf(30));
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(new URL("http://droidtestlab.com/share.html").toURI());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}));
}
}
}
private void openHelpWindow() {
Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(new URL("http://droidtestlab.com/help").toURI());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public void showNewVersionAvailable(String version) {
final JPanel tmp = new JPanel(new VerticalLayout(5));
tmp.setBorder(new EmptyBorder(10, 10, 20, 10));
JLabel label1 = new JLabel("<html> New version " + version + " of Android Test Recorder is available <a href=\"\">click here</a> to install.</html>");
label1.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, "Plugins");
}
});
label1.setHorizontalAlignment(JLabel.CENTER);
tmp.add(label1);
JLabel label2 = new JLabel("<html> Or <a href=\"\">click here</a> to hide this message.</html>");
label2.setHorizontalAlignment(JLabel.CENTER);
label2.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
panel.remove(tmp);
panel.repaint();
}
});
tmp.add(label2);
panel.add(tmp, "South");
panel.repaint();
}
private void fillActivities(ModulesComboBoxModel.ModuleWrapper module) {
List<Activity> activities = Collections.emptyList();
Activity selected = null;
if (module != null) {
FacetManager facetManager = FacetManager.getInstance(module.getModule());
com.intellij.facet.Facet[] facets = facetManager.getAllFacets();
Iterator i$;
Activity activity;
for (int i = 0; i < facets.length; i++) {
com.intellij.facet.Facet facet = facets[i];
if ((facet instanceof AndroidFacet)) {
AndroidFacet androidFacet = (AndroidFacet) facet;
org.jetbrains.android.dom.manifest.Manifest manifest = androidFacet.getManifest();
activities = manifest.getApplication().getActivities();
for (i$ = activities.iterator(); i$.hasNext(); ) {
activity = (Activity) i$.next();
for (org.jetbrains.android.dom.manifest.IntentFilter filter : activity.getIntentFilters()) {
for (org.jetbrains.android.dom.manifest.Action action : filter.getActions()) {
if (action.getName().getValue().equals("android.intent.action.MAIN")) {
selected = activity;
this.activitiesBoxModel = new ActivitiesComboBoxModel(activities, selected);
this.activitiesList.setModel(this.activitiesBoxModel);
this.activitiesList.setPrototypeDisplayValue(selected);
this.recButton.setEnabled(this.activitiesBoxModel.getSelected() != null);
return;
}
}
}
}
}
}
}
this.activitiesBoxModel = new ActivitiesComboBoxModel(activities, selected);
this.activitiesList.setModel(this.activitiesBoxModel);
this.recButton.setEnabled(false);
}
private void stop(AnActionEvent event) {
if (this.executionChecker != null && this.executionChecker.getDescriptor() != null && this.executionChecker.getDescriptor().getProcessHandler() != null) {
this.executionChecker.getDescriptor().getProcessHandler().destroyProcess();
}
AccessToken token = WriteAction.start();
try {
final GradleBuildFile buildFile = GradleBuildFile.get(currentModule);
final String buildFilePath = buildFile.getFile().getPath();
File buildF = new File(buildFilePath);
File dir = buildF.getParentFile();
File saved = new File(dir, GRADLE_BUILD_SAVED);
if (saved.exists()) {
// restore gradle build from saved file
final byte[] data = Files.readAllBytes(saved.toPath());
new WriteCommandAction<Void>(project, "Test Recorder Stop", buildFile.getPsiFile()) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
buildFile.getFile().setBinaryContent(data);
}
}.execute();
saved.delete();
} else {
// saved file not found, so simple remove dependency
if (buildFile != null) {
final List<BuildFileStatement> dependencies = buildFile.getDependencies();
final Dependency dependency = findDepRecord(dependencies);
if (dependency != null) {
dependencies.remove(dependency);
new WriteCommandAction<Void>(project, "Test Recorder Stop", buildFile.getPsiFile()) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
buildFile.setValue(BuildFileKey.DEPENDENCIES, dependencies);
}
}.execute();
}
}
}
if (this.testVirtualFile != null) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
try {
ToolsTestsRecorderAction.this.testVirtualFile.delete(null);
} catch (IOException e) {
Messages.showErrorDialog(ToolsTestsRecorderAction.this.project, "Failed to delete file " + ToolsTestsRecorderAction.this.testVirtualFile.getCanonicalPath(), "Error");
e.printStackTrace();
}
}
});
}
} catch (IOException e) {
e.printStackTrace();
} finally {
token.finish();
}
GradleProjectImporter.getInstance().requestProjectSync(project, null);
if (eventReader != null) {
eventReader.stop();
}
}
private void record(AnActionEvent event) {
project = ((Project) event.getData(PlatformDataKeys.PROJECT));
currentModule = ((ModulesComboBoxModel.ModuleWrapper) this.moduleBoxModel.getSelected()).getModule();
final String packageName;
PsiFile psiFile;
AccessToken token = WriteAction.start();
try {
final GradleBuildFile buildFile = GradleBuildFile.get(currentModule);
// save a copy of gradle build file
String buildFilePath = buildFile.getFile().getPath();
File buildF = new File(buildFilePath);
File dir = buildF.getParentFile();
File saved = new File(dir, GRADLE_BUILD_SAVED);
if (saved.exists()) {
saved.delete();
}
Files.copy(buildF.toPath(), saved.toPath());
final List<BuildFileStatement> dependencies = buildFile.getDependencies();
boolean espressoFound = false;
for (BuildFileStatement statement : dependencies) {
if ((statement instanceof Dependency)) {
Dependency dependency = (Dependency) statement;
if ((dependency.type == Dependency.Type.EXTERNAL) && (dependency.scope == com.android.tools.idea.gradle.parser.Dependency.Scope.ANDROID_TEST_COMPILE) && (dependency.data != null) && (dependency.data.toString().startsWith("com.android.support.test.espresso:espresso-core"))) {
espressoFound = true;
break;
}
}
}
if (!espressoFound) {
Messages.showErrorDialog(this.project, "<html>Failed to find dependencies for Espresso. You must set up Espresso as defined at " +
"<a href='http://developer.android.com/training/testing/start/index.html#config-instrumented-tests'>http://developer.android.com/training/testing/start/index.html#config-instrumented-tests</a></html>", "Error");
this.recButton.setText(RECORD);
this.recButton.setIcon(IconLoader.getIcon("icons/rec.png"));
return;
}
Dependency dependency = findDepRecord(dependencies);
if (dependency == null) {
dependencies.add(new Dependency(com.android.tools.idea.gradle.parser.Dependency.Scope.COMPILE, Dependency.Type.FILES, ToolsTestsRecorderAction.this.jarPath));
new WriteCommandAction<Void>(project, "Test Recorder Start", buildFile.getPsiFile()) {
@Override
protected void run(@NotNull Result<Void> result) throws Throwable {
buildFile.setValue(BuildFileKey.DEPENDENCIES, dependencies);
}
}.execute();
}
uniqueId = System.currentTimeMillis();
Activity activity = ((ActivitiesComboBoxModel.ActivityWrapper) this.activitiesBoxModel.getSelected()).getActivity();
final PsiClass activityClass = (PsiClass) activity.getActivityClass().getValue();
com.intellij.psi.PsiManager manager = com.intellij.psi.PsiManager.getInstance(this.project);
psiFile = activityClass.getContainingFile();
final PsiDirectory psiDirectory = psiFile.getContainingDirectory();
packageName = ((com.intellij.psi.PsiJavaFile) activityClass.getContainingFile()).getPackageName();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
try {
PsiFile testFile = psiDirectory.findFile(TEST_FILE_NAME);
if (testFile == null) {
testFile = psiDirectory.createFile(TEST_FILE_NAME);
}
ToolsTestsRecorderAction.this.testVirtualFile = testFile.getVirtualFile();
com.intellij.openapi.vfs.VfsUtil.saveText(ToolsTestsRecorderAction.this.testVirtualFile,
ToolsTestsRecorderAction.template.replace("{ACTIVITY}", activityClass.getName()).replace("{PACKAGE}", packageName).
replace("{CLASSNAME}", "AndrTestRec").replace("{ID}", String.valueOf(uniqueId)));
} catch (IOException e) {
Messages.showErrorDialog(ToolsTestsRecorderAction.this.project, e.getMessage(), "Error");
}
}
});
} catch (IOException e) {
e.printStackTrace();
Messages.showErrorDialog(this.project, "IO error : " + e.toString(), "Error");
return;
} finally {
token.finish();
}
RunManager runManager = RunManager.getInstance(this.project);
ConfigurationFactory factory;
RunConfiguration configuration;
String runConfigName = RUN_CONFIG_NAME + System.currentTimeMillis();
try {
// for Android Studio less them 1.5
org.jetbrains.android.run.testing.AndroidTestRunConfigurationType configurationType = new org.jetbrains.android.run.testing.AndroidTestRunConfigurationType();
factory = configurationType.getFactory();
org.jetbrains.android.run.testing.AndroidTestRunConfiguration runConfiguration = new org.jetbrains.android.run.testing.AndroidTestRunConfiguration(this.project, factory);
runConfiguration.setName(runConfigName);
runConfiguration.setModule(currentModule);
runConfiguration.setTargetSelectionMode(org.jetbrains.android.run.TargetSelectionMode.SHOW_DIALOG);
runConfiguration.TESTING_TYPE = 2;
runConfiguration.CLASS_NAME = (packageName + "." + ANDR_TEST_CLASSNAME);
configuration = runConfiguration;
} catch (NoClassDefFoundError e) {
// for Android Studio more or equals them 1.5
com.android.tools.idea.run.testing.AndroidTestRunConfigurationType configurationType = new com.android.tools.idea.run.testing.AndroidTestRunConfigurationType();
factory = configurationType.getFactory();
com.android.tools.idea.run.testing.AndroidTestRunConfiguration runConfiguration = new com.android.tools.idea.run.testing.AndroidTestRunConfiguration(this.project, factory);
runConfiguration.setName(runConfigName);
runConfiguration.setModule(currentModule);
runConfiguration.setTargetSelectionMode(com.android.tools.idea.run.TargetSelectionMode.SHOW_DIALOG);
runConfiguration.TESTING_TYPE = 2;
runConfiguration.CLASS_NAME = (packageName + "." + ANDR_TEST_CLASSNAME);
configuration = runConfiguration;
}
com.intellij.execution.RunnerAndConfigurationSettings rcs = runManager.createConfiguration(configuration, factory);
if (PluginConfiguration.isLeaveRunConfiguration()) {
rcs.setTemporary(false);
runManager.addConfiguration(rcs, true);
} else {
rcs.setTemporary(true);
}
ExecutionManager executionManager = ExecutionManager.getInstance(this.project);
executionManager.restartRunProfile(this.project, com.intellij.execution.executors.DefaultRunExecutor.getRunExecutorInstance(), com.intellij.execution.DefaultExecutionTarget.INSTANCE, rcs, (ProcessHandler) null);
this.executionChecker = new ExecutionChecker(executionManager, runConfigName, this);
new java.util.Timer().schedule(this.executionChecker, 200L, 200L);
eventsList.clear(project, currentModule, psiFile);
}
private Dependency findDepRecord(List<BuildFileStatement> dependencies) {
for (BuildFileStatement statement : dependencies) {
if ((statement instanceof Dependency)) {
Dependency dependency = (Dependency) statement;
if ((dependency.type == Dependency.Type.FILES) && (dependency.data != null) && (dependency.data.toString().equals(this.jarPath))) {
return dependency;
}
}
}
return null;
}
/*
String s under unix example:
jar:file:/home/vpedak/.AndroidStudio1.4/config/plugins/AndroidTestsRecorder/lib/AndroidTestsRecorder.jar!/com/vpedak/testsrecorder/plugin/actions/ToolsTestsRecorderAction.class
*/
private String getJarPath() {
String name = this.getClass().getName().replace('.', '/');
String s = this.getClass().getResource("/" + name + ".class").toString();
//Messages.showInfoMessage(s, "info");
s = s.substring(0, s.indexOf(".jar") + 4);
String os = System.getProperty("os.name").toLowerCase();
if (os.indexOf("win") >= 0) {
s = s.substring(s.lastIndexOf(':') - 1);
} else {
s = s.substring(s.indexOf("file:") + 5);
}
if (s.indexOf('%') != -1) {
try {
s = java.net.URLDecoder.decode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
System.err.println("UTF-8 is unsupported");
}
}
return s;
// temporary because we are starting plugin from Idea and it is not packaged in ZIP
//return "/home/vpedak/IdeaProjects/droidtestrec/AndroidTestsRecorder.jar";
}
public void testStarted() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
ToolsTestsRecorderAction.this.toolWindow.activate(null, true, true);
}
});
eventReader.start(uniqueId);
}
}