Java tutorial
/* * Copyright (C) 2016 Bjrn Quentin * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.mobilej.plugin.adc; import com.android.ddmlib.*; import com.android.tools.idea.ddms.EdtExecutor; import com.android.tools.idea.ddms.adb.AdbService; import com.android.tools.idea.model.AndroidModel; import com.android.tools.idea.monitor.AndroidToolWindowFactory; import com.google.common.io.LineReader; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.intellij.facet.ProjectFacetManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.util.XmlPullUtil; import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; import java.io.*; import java.text.MessageFormat; import java.util.*; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; /** * Android Device Controller Plugin for Android Studio */ public class ToolWindowFactory implements com.intellij.openapi.wm.ToolWindowFactory { private static ResourceBundle resourceBundle = ResourceBundle.getBundle("de.mobilej.plugin.adc.Plugin"); private static LocaleData[] LOCALES; static { ArrayList<LocaleData> data = new ArrayList<>(); LineReader lr = null; try { InputStream is = ToolWindowFactory.class.getResourceAsStream("/de/mobilej/plugin/adc/locales.txt"); lr = new LineReader(new InputStreamReader(is, "UTF-8")); String line = null; while ((line = lr.readLine()) != null) { if (line.indexOf("_") > 0 && line.indexOf("[") > 0) { String lang = line.substring(0, line.indexOf("_")); String cntry = line.substring(line.indexOf("_") + 1, line.indexOf(" ")); String desc = line.substring(line.indexOf("[") + 1, line.length() - 1); LocaleData ld = new LocaleData(desc, lang, cntry); data.add(ld); } } } catch (IOException ioe) { // ignored } LOCALES = data.toArray(new LocaleData[data.size()]); } private ComboBox devices; private AndroidDebugBridge adBridge; private JButton inputOnDeviceButton; private JButton clearDataButton; private static class StringShellOutputReceiver implements IShellOutputReceiver { private StringBuffer result = new StringBuffer(); void reset() { result.delete(0, result.length()); } String getResult() { return result.toString(); } @Override public void addOutput(byte[] bytes, int i, int i1) { try { result.append(new String(bytes, i, i1, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void flush() { } @Override public boolean isCancelled() { return false; } } private StringShellOutputReceiver rcv = new StringShellOutputReceiver(); private AndroidDebugBridge.IDeviceChangeListener deviceChangeListener = new AndroidDebugBridge.IDeviceChangeListener() { @Override public void deviceConnected(IDevice iDevice) { updateDeviceComboBox(); } @Override public void deviceDisconnected(IDevice iDevice) { updateDeviceComboBox(); } @Override public void deviceChanged(IDevice iDevice, int i) { } }; private ActionListener deviceSelectedListener = e -> updateFromDevice(); private JBCheckBox showLayoutBounds; private ComboBox localeChooser; private boolean userAction = false; private JButton goToActivityButton; private final Storage storage = ServiceManager.getService(Storage.class); public ToolWindowFactory() { } // Create the tool window content. public void createToolWindowContent(@NotNull final Project project, @NotNull final ToolWindow toolWindow) { ContentFactory contentFactory = ContentFactory.SERVICE.getInstance(); JPanel framePanel = createPanel(project); disableAll(); final File adb = AndroidSdkUtils.getAdb(project); if (adb == null) { return; } ListenableFuture<AndroidDebugBridge> future = AdbService.getInstance().getDebugBridge(adb); Futures.addCallback(future, new FutureCallback<AndroidDebugBridge>() { @Override public void onSuccess(@Nullable AndroidDebugBridge bridge) { ToolWindowFactory.this.adBridge = bridge; Logger.getInstance(AndroidToolWindowFactory.class).info("Successfully obtained debug bridge"); AndroidDebugBridge.addDeviceChangeListener(deviceChangeListener); updateDeviceComboBox(); } @Override public void onFailure(@NotNull Throwable t) { // If we cannot connect to ADB in a reasonable amount of time (10 seconds timeout in AdbService), then something is seriously // wrong. The only identified reason so far is that some machines have incompatible versions of adb that were already running. // e.g. Genymotion, some HTC flashing software, Ubuntu's adb package may all conflict with the version of adb in the SDK. Logger.getInstance(AndroidToolWindowFactory.class).info("Unable to obtain debug bridge", t); String msg = MessageFormat.format(resourceBundle.getString("error.message.adb"), adb.getAbsolutePath()); Messages.showErrorDialog(msg, resourceBundle.getString("error.title.adb")); } }, EdtExecutor.INSTANCE); Content content = contentFactory.createContent(framePanel, "", false); toolWindow.getContentManager().addContent(content); } @NotNull private JPanel createPanel(@NotNull Project project) { // Create Panel and Content JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.NONE; c.anchor = GridBagConstraints.LINE_START; devices = new ComboBox(new String[] { resourceBundle.getString("device.none") }); c.gridx = 0; c.gridy = 0; panel.add(new JLabel("Device"), c); c.gridx = 1; c.gridy = 0; panel.add(devices, c); showLayoutBounds = new JBCheckBox(resourceBundle.getString("show.layout.bounds")); c.gridx = 0; c.gridy = 1; c.gridwidth = 2; panel.add(showLayoutBounds, c); showLayoutBounds.addActionListener(e -> { final String what = showLayoutBounds.isSelected() ? "true" : "\"\""; final String cmd = "setprop debug.layout " + what; ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); userAction = true; executeShellCommand(cmd, true); userAction = false; }, resourceBundle.getString("setting.values.title"), false, null); }); localeChooser = new ComboBox(LOCALES); c.gridx = 0; c.gridy = 2; c.gridwidth = 2; panel.add(localeChooser, c); localeChooser.addActionListener(e -> { final LocaleData ld = (LocaleData) localeChooser.getSelectedItem(); if (ld == null) { return; } ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); userAction = true; executeShellCommand( "am start -a SETMYLOCALE --es language " + ld.language + " --es country " + ld.county, false); userAction = false; }, resourceBundle.getString("setting.values.title"), false, null); }); goToActivityButton = new JButton(resourceBundle.getString("button.goto_activity")); c.gridx = 0; c.gridy = 3; c.gridwidth = 2; panel.add(goToActivityButton, c); goToActivityButton .addActionListener(e -> ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); userAction = true; final String result = executeShellCommand("dumpsys activity top", false); userAction = false; if (result == null) { return; } ApplicationManager.getApplication().invokeLater(() -> { String activity = result.substring(result.indexOf("ACTIVITY ") + 9); activity = activity.substring(0, activity.indexOf(" ")); String pkg = activity.substring(0, activity.indexOf("/")); String clz = activity.substring(activity.indexOf("/") + 1); if (clz.startsWith(".")) { clz = pkg + clz; } GlobalSearchScope scope = GlobalSearchScope.allScope(project); PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(clz, scope); if (psiClass != null) { FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); //Open the file containing the class VirtualFile vf = psiClass.getContainingFile().getVirtualFile(); //Jump there new OpenFileDescriptor(project, vf, 1, 0).navigateInEditor(project, false); } else { Messages.showMessageDialog(project, clz, resourceBundle.getString("error.class_not_found"), Messages.getWarningIcon()); return; } }); }, resourceBundle.getString("setting.values.title"), false, null)); inputOnDeviceButton = new JButton(resourceBundle.getString("button.input_on_device")); c.gridx = 0; c.gridy = 4; c.gridwidth = 2; panel.add(inputOnDeviceButton, c); inputOnDeviceButton.addActionListener(e -> { final String text2send = Messages.showMultilineInputDialog(project, resourceBundle.getString("send_text.message"), resourceBundle.getString("send_text.title"), storage.getLastSentText(), Messages.getQuestionIcon(), null); if (text2send != null) { storage.setLastSentText(text2send); ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); userAction = true; doInputOnDevice(text2send); userAction = false; }, resourceBundle.getString("processing.title"), false, null); } }); clearDataButton = new JButton(resourceBundle.getString("button.clear_data")); c.gridx = 0; c.gridy = 5; c.gridwidth = 2; panel.add(clearDataButton, c); clearDataButton.addActionListener(actionEvent -> { ArrayList<String> appIds = new ArrayList<String>(); List<AndroidFacet> androidFacets = ProjectFacetManager.getInstance(project).getFacets(AndroidFacet.ID); if (androidFacets != null) { for (AndroidFacet facet : androidFacets) { if (!facet.isLibraryProject()) { AndroidModel androidModel = facet.getAndroidModel(); if (androidModel != null) { String appId = androidModel.getApplicationId(); appIds.add(appId); } } } } ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); userAction = true; for (String appId : appIds) { executeShellCommand("pm clear " + appId, false); } userAction = false; }, resourceBundle.getString("processing.title"), false, null); }); JPanel framePanel = new JPanel(new BorderLayout()); framePanel.add(panel, BorderLayout.NORTH); return framePanel; } private void doInputOnDevice(String text2send) { /* Syntax: `tap 130 150` -> sends "input tap 130 150" .... really just "input " and append the command `tap @my.package:id/text` -> first find the center of the given res-id (uiautomator dump /dev/tty) `tap @*:id/text` -> support wildcards `swipe 10 20 30 40` -> simple swipe (px) `swipe @*:id/my_id[10,20] @*:id/my_id[50,60]` -> swipe from 10% of x of given view, 20% of y of given view to 50% of x of the view to 60% of y of the view `tap @*:id/button[20,30]` syntax also works in general for @id things ``` -> escapes ` ` ` -> new line which is not sent `#500` -> wait 500 milliseconds */ text2send = text2send.replace("```", "\u2764"); text2send = text2send + "`"; StringTokenizer tokenizer = new StringTokenizer(text2send, "`", true); boolean inCommand = false; String plainText = null; String commandText = null; while (tokenizer.hasMoreElements()) { String next = tokenizer.nextToken(); if ("`".equals(next)) { if (!inCommand) { inCommand = true; if (plainText != null) { String escaped = plainText.replace("\"", "\\\"").replace("\u2764", "\\`"); executeShellCommand("input text \"" + escaped + "\"", false); plainText = null; commandText = null; } } else { inCommand = false; if (commandText != null) { commandText = commandText.replace("\r", "").replace("\n", ""); if (commandText.length() > 0) { if (commandText.startsWith("#")) { long timeToWait = Long.parseLong(commandText.substring(1)); try { Thread.sleep(timeToWait); } catch (InterruptedException e) { // do nothing } } else { if (commandText.contains("@")) { commandText = processViewIds(commandText); } executeShellCommand("input " + commandText, false); } } commandText = null; plainText = null; } } } else { if (inCommand) { commandText = next; plainText = null; } else { plainText = next; commandText = null; } } } } private String processViewIds(String commandText) { /* @<id, supporting wildcards, if no ":" contained it will prepend "*:"> defaults to center of view @<id, supporting wildcards, if no ":" contained it will prepend "*:">[percentX,percentY] percentX/Y in view bounds */ String views = executeShellCommand("uiautomator dump /dev/tty", false); views = views.substring(0, views.lastIndexOf(">") + 1); HashMap<String, Rectangle> resIdToBoundsMap = new HashMap<>(); try { XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); xpp.setInput(new StringReader(views)); int eventType; while ((eventType = xpp.getEventType()) != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { if ("node".equals(xpp.getName())) { String bounds = XmlPullUtil.getAttributeValue(xpp, "bounds"); String resId = XmlPullUtil.getAttributeValue(xpp, "resource-id"); if (resId != null && resId.length() > 0) { bounds = bounds.replace("][", ","); bounds = bounds.replace("[", ""); bounds = bounds.replace("]", ""); String[] coords = bounds.split(","); int x1 = Integer.parseInt(coords[0]); int y1 = Integer.parseInt(coords[1]); int x2 = Integer.parseInt(coords[2]); int y2 = Integer.parseInt(coords[3]); Rectangle rect = new Rectangle(x1, y1, x2 - x1, y2 - y1); resIdToBoundsMap.put(resId, rect); } } } xpp.next(); } } catch (XmlPullParserException | IOException e) { e.printStackTrace(); } while (commandText.contains("@")) { int idx = commandText.indexOf("@"); StringBuilder sb = new StringBuilder(); int i = idx; while (i < commandText.length() && commandText.charAt(i) > ' ') { sb.append(commandText.charAt(i)); i++; } String calculatedCoords = "0 0"; String resIdToMatch = sb.substring(1); double percentX = 0.5; double percentY = 0.5; if (resIdToMatch.contains("[") && resIdToMatch.contains("]")) { String percentPart = resIdToMatch.substring(resIdToMatch.indexOf("[") + 1, resIdToMatch.indexOf("]")); resIdToMatch = resIdToMatch.substring(0, resIdToMatch.indexOf("[")); String[] percentParts = percentPart.split(","); try { percentX = Double.parseDouble(percentParts[0]) / 100; percentY = Double.parseDouble(percentParts[1]) / 100; } catch (NumberFormatException nfe) { nfe.printStackTrace(); } } Rectangle rect = new Rectangle(0, 0, 0, 0); if (resIdToBoundsMap.containsKey(resIdToMatch)) { rect = resIdToBoundsMap.get(resIdToMatch); } else { if (!resIdToMatch.contains(":")) { resIdToMatch = "*:" + resIdToMatch; } for (Map.Entry<String, Rectangle> entry : resIdToBoundsMap.entrySet()) { if (wildcardMatch(resIdToMatch, entry.getKey())) { rect = entry.getValue(); break; } } } calculatedCoords = "" + (int) (rect.x + rect.width * percentX) + " " + (int) (rect.y + rect.height * percentY); commandText = commandText.substring(0, idx) + calculatedCoords + commandText.substring(idx + sb.length()); } return commandText; } public static boolean wildcardMatch(final String toMatch, final String value) { StringBuilder patternStringBuilder = new StringBuilder(); for (final char c : toMatch.toCharArray()) { switch (c) { case '?': patternStringBuilder.append(".?"); break; case '*': patternStringBuilder.append(".*"); break; default: patternStringBuilder.append(Pattern.quote(String.valueOf(c))); break; } } Pattern pattern = Pattern.compile(patternStringBuilder.toString()); return pattern.matcher(value).matches(); } private void updateFromDevice() { ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); IDevice selectedDevice = getSelectedDevice(); if (selectedDevice != null) { setupDevice(selectedDevice); String debugLayoutProperty = getSysPropFromDevice("debug.layout", selectedDevice); if ("true".equals(debugLayoutProperty)) { showLayoutBounds.setSelected(true); } else { showLayoutBounds.setSelected(false); } String deviceLocale = getSysPropFromDevice("persist.sys.locale", selectedDevice); int i = 0; for (LocaleData ld : LOCALES) { if (deviceLocale != null && deviceLocale.startsWith(ld.language) && deviceLocale.endsWith(ld.county)) { final int toSelect = i; SwingUtilities.invokeLater(() -> localeChooser.setSelectedIndex(toSelect)); break; } i++; } SwingUtilities.invokeLater(() -> enableAll()); } else { SwingUtilities.invokeLater(() -> disableAll()); } }, resourceBundle.getString("initializing.device.message"), false, null); } private String getSysPropFromDevice(String propName, IDevice selectedDevice) { try { return selectedDevice.getSystemProperty(propName).get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } return null; } private void disableAll() { showLayoutBounds.setEnabled(false); localeChooser.setEnabled(false); goToActivityButton.setEnabled(false); inputOnDeviceButton.setEnabled(false); clearDataButton.setEnabled(false); } private void enableAll() { showLayoutBounds.setEnabled(true); localeChooser.setEnabled(true); goToActivityButton.setEnabled(true); inputOnDeviceButton.setEnabled(true); clearDataButton.setEnabled(true); } private void setupDevice(final IDevice selectedDevice) { try { installEnablerApk(selectedDevice); } catch (IOException e) { e.printStackTrace(); } } @SuppressWarnings("unchecked") private void updateDeviceComboBox() { devices.removeActionListener(deviceSelectedListener); String selectedDevice = (String) devices.getSelectedItem(); IDevice[] devs = adBridge.getDevices(); Vector devicesList = new Vector(); devicesList.add("-- none --"); for (IDevice device : devs) { devicesList.add(device.toString()); } devices.setModel(new DefaultComboBoxModel<>(devicesList)); if (devicesList.size() == 1) { disableAll(); } else { devices.setSelectedItem(selectedDevice); devices.setSelectedItem(devices.getSelectedItem()); if (devices.getSelectedIndex() == 0) { disableAll(); } else { enableAll(); } } devices.addActionListener(deviceSelectedListener); } private String executeShellCommand(String cmd, boolean doPoke) { if (!userAction) { return null; } if (devices.getSelectedIndex() == 0) { return null; } String res = null; String selDevice = (String) devices.getSelectedItem(); for (IDevice device : adBridge.getDevices()) { if (selDevice.equals(device.toString())) { try { rcv.reset(); device.executeShellCommand(cmd, rcv); res = rcv.getResult(); if (doPoke) { device.executeShellCommand("am start -a POKESYSPROPS", rcv); } } catch (TimeoutException | AdbCommandRejectedException | ShellCommandUnresponsiveException | IOException e1) { e1.printStackTrace(); } } } return res; } private void installEnablerApk(IDevice device) throws IOException { // TODO no need to create the tmp file over and over again File tmpfile = File.createTempFile("enabler", "apk"); FileOutputStream fos = null; InputStream is = null; try { is = getClass().getResourceAsStream("/de/mobilej/plugin/adc/enabler.apk"); fos = new FileOutputStream(tmpfile); byte[] buffer = new byte[4096]; int len = 0; while ((len = is.read(buffer)) > 0) { fos.write(buffer, 0, len); } } finally { if (fos != null) { fos.flush(); fos.close(); } if (is != null) { is.close(); } } try { device.installPackage(tmpfile.getAbsolutePath(), false); } catch (InstallException ie) { ie.printStackTrace(); } try { device.executeShellCommand( "pm grant mobilej.de.systemproppoker android.permission.CHANGE_CONFIGURATION", rcv); } catch (Exception e) { e.printStackTrace(); } } private IDevice getSelectedDevice() { String selDevice = (String) devices.getSelectedItem(); for (IDevice device : adBridge.getDevices()) { if (selDevice.equals(device.toString())) { return device; } } return null; } /** * Holder for Locale Data */ private static class LocaleData { final String name; final String language; final String county; LocaleData(String name, String language, String county) { this.name = name; this.language = language; this.county = county; } @Override public String toString() { return name; } } }