/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-16 The Processing Foundation
Copyright (c) 2009-12 Ben Fry and Casey Reas
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.mode.android;
import processing.app.Base;
import processing.app.Mode;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.Settings;
import processing.app.SketchException;
import processing.app.ui.EditorException;
import processing.app.ui.EditorState;
import processing.app.ui.EditorToolbar;
import processing.app.ui.Toolkit;
import processing.core.PApplet;
import processing.mode.java.JavaEditor;
import processing.mode.java.preproc.PdePreprocessor;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.TimerTask;
@SuppressWarnings("serial")
public class AndroidEditor extends JavaEditor {
private int appComponent;
private Settings settings;
private boolean resetManifest = false;
private AndroidMode androidMode;
private java.util.Timer updateDevicesTimer;
private JCheckBoxMenuItem fragmentItem;
private JCheckBoxMenuItem wallpaperItem;
private JCheckBoxMenuItem watchfaceItem;
private JCheckBoxMenuItem cardboardItem;
private static final String USB_DRIVER_TITLE = "USB Driver warning";
private static final String USB_DRIVER_URL =
"http://developer.android.com/sdk/win-usb.html";
private static final String USB_DRIVER_MESSAGEA =
"You might need to install Google USB Driver to run " +
"the sketch on your device. Please follow this guide " +
"to install the driver.
";
private static final String USB_DRIVER_MESSAGEB =
"
For your reference, the driver is located in:
";
protected AndroidEditor(Base base, String path, EditorState state,
Mode mode) throws EditorException {
super(base, path, state, mode);
loadModeSettings();
androidMode = (AndroidMode) mode;
androidMode.resetUserSelection();
androidMode.checkSDK(this);
}
@Override
public PdePreprocessor createPreprocessor(final String sketchName) {
return new AndroidPreprocessor(sketchName);
}
class UpdateDeviceListTask extends TimerTask {
private JMenu deviceMenu;
public UpdateDeviceListTask(JMenu deviceMenu) {
this.deviceMenu = deviceMenu;
}
private Device selectFirstNonWatchDevice(java.util.List deviceList) {
for (Device device : deviceList) {
if (device.hasFeature("watch")) {
// Don't include the watch devices to the list, they get their watch
// faces through the handheld they are paired with.
continue;
}
return device;
}
return null;
}
@Override
public void run() {
if (androidMode == null || androidMode.getSDK() == null) return;
if (appComponent == AndroidBuild.WATCHFACE) {
Devices.enableBlueToothDebugging();
}
final Devices devices = Devices.getInstance();
java.util.List deviceList = devices.findMultiple(false);
Device selectedDevice = devices.getSelectedDevice();
if (deviceList.size() == 0) {
//if (deviceMenu.getItem(0).isEnabled()) {
if (0 < deviceMenu.getItemCount()) {
deviceMenu.removeAll();
JMenuItem noDevicesItem = new JMenuItem("No connected devices");
noDevicesItem.setEnabled(false);
deviceMenu.add(noDevicesItem);
}
devices.setSelectedDevice(null);
} else {
deviceMenu.removeAll();
if (selectedDevice == null) {
selectedDevice = selectFirstNonWatchDevice(deviceList);
devices.setSelectedDevice(selectedDevice);
} else {
// check if selected device is still connected
boolean found = false;
for (Device device : deviceList) {
if (device.equals(selectedDevice)) {
found = true;
break;
}
}
if (!found) {
selectedDevice = selectFirstNonWatchDevice(deviceList);
devices.setSelectedDevice(selectedDevice);
}
}
for (final Device device : deviceList) {
if (device.hasFeature("watch")) {
// Don't include the watch devices to the list, they get their watch
// faces through the handheld they are paired with.
continue;
}
final JCheckBoxMenuItem deviceItem = new JCheckBoxMenuItem(device.getName());
deviceItem.setEnabled(true);
if (device.equals(selectedDevice)) deviceItem.setState(true);
// prevent checkboxmenuitem automatic state changing onclick
deviceItem.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (device.equals(devices.getSelectedDevice())) deviceItem.setState(true);
else deviceItem.setState(false);
}
});
deviceItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
devices.setSelectedDevice(device);
for (int i = 0; i < deviceMenu.getItemCount(); i++) {
((JCheckBoxMenuItem) deviceMenu.getItem(i)).setState(false);
}
deviceItem.setState(true);
}
});
deviceMenu.add(deviceItem);
}
}
}
}
public EditorToolbar createToolbar() {
return new AndroidToolbar(this, base);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public JMenu buildFileMenu() {
String exportPkgTitle = AndroidToolbar.getTitle(AndroidToolbar.EXPORT, false);
JMenuItem exportPackage = Toolkit.newJMenuItem(exportPkgTitle, 'E');
exportPackage.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleExportPackage();
}
});
String exportProjectTitle = AndroidToolbar.getTitle(AndroidToolbar.EXPORT, true);
JMenuItem exportProject = Toolkit.newJMenuItemShift(exportProjectTitle, 'E');
exportProject.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleExportProject();
}
});
return buildFileMenu(new JMenuItem[] { exportPackage, exportProject});
}
public JMenu buildSketchMenu() {
JMenuItem runItem = Toolkit.newJMenuItem(AndroidToolbar.getTitle(AndroidToolbar.RUN, false), 'R');
runItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleRunDevice();
}
});
JMenuItem presentItem = Toolkit.newJMenuItemShift(AndroidToolbar.getTitle(AndroidToolbar.RUN, true), 'R');
presentItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleRunEmulator();
}
});
JMenuItem stopItem = new JMenuItem(AndroidToolbar.getTitle(AndroidToolbar.STOP, false));
stopItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
handleStop();
}
});
return buildSketchMenu(new JMenuItem[] { runItem, presentItem, stopItem });
}
public JMenu buildModeMenu() {
JMenu menu = new JMenu("Android");
JMenuItem item;
item = new JMenuItem("Sketch Permissions");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new Permissions(sketch, appComponent, androidMode.getFolder());
}
});
menu.add(item);
menu.addSeparator();
fragmentItem = new JCheckBoxMenuItem("App");
wallpaperItem = new JCheckBoxMenuItem("Wallpaper");
watchfaceItem = new JCheckBoxMenuItem("Watch Face");
cardboardItem = new JCheckBoxMenuItem("Cardboard");
fragmentItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setAppComponent(AndroidBuild.FRAGMENT);
fragmentItem.setState(true);
wallpaperItem.setState(false);
watchfaceItem.setSelected(false);
cardboardItem.setSelected(false);
androidMode.showSelectComponentMessage(AndroidBuild.FRAGMENT);
}
});
wallpaperItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setAppComponent(AndroidBuild.WALLPAPER);
fragmentItem.setState(false);
wallpaperItem.setState(true);
watchfaceItem.setSelected(false);
cardboardItem.setSelected(false);
androidMode.showSelectComponentMessage(AndroidBuild.WALLPAPER);
}
});
watchfaceItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setAppComponent(AndroidBuild.WATCHFACE);
fragmentItem.setState(false);
wallpaperItem.setState(false);
watchfaceItem.setSelected(true);
cardboardItem.setSelected(false);
androidMode.showSelectComponentMessage(AndroidBuild.WATCHFACE);
}
});
cardboardItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setAppComponent(AndroidBuild.CARDBOARD);
fragmentItem.setState(false);
wallpaperItem.setState(false);
watchfaceItem.setSelected(false);
cardboardItem.setSelected(true);
androidMode.showSelectComponentMessage(AndroidBuild.CARDBOARD);
}
});
fragmentItem.setState(false);
wallpaperItem.setState(false);
watchfaceItem.setSelected(false);
cardboardItem.setSelected(false);
menu.add(fragmentItem);
menu.add(wallpaperItem);
menu.add(watchfaceItem);
menu.add(cardboardItem);
menu.addSeparator();
final JMenu mobDeveMenu = new JMenu("Devices");
JMenuItem noMobDevItem = new JMenuItem("No connected devices");
noMobDevItem.setEnabled(false);
mobDeveMenu.add(noMobDevItem);
menu.add(mobDeveMenu);
// start updating device menus
UpdateDeviceListTask task = new UpdateDeviceListTask(mobDeveMenu);
if (updateDevicesTimer == null) {
updateDevicesTimer = new java.util.Timer();
} else {
updateDevicesTimer.cancel();
}
updateDevicesTimer.schedule(task, 5000, 5000);
menu.addSeparator();
/*
// TODO: The SDK selection menu will be removed once app publishing is fully
// functional: correct minimum SDK level can be inferred from app type
// (fragment, wallpaper, etc), and target SDK from the highest available in
// the SDK.
final JMenu sdkMenu = new JMenu("Target SDK");
JMenuItem defaultItem = new JCheckBoxMenuItem("No available targets");
defaultItem.setEnabled(false);
sdkMenu.add(defaultItem);
new Thread() {
@Override
public void run() {
while(androidMode == null || androidMode.getSDK() == null) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
updateSdkMenu(sdkMenu);
}
}.start();
menu.add(sdkMenu);
menu.addSeparator();
*/
item = new JMenuItem("SDK Manager");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
File file = androidMode.getSDK().getAndroidTool();
try {
Runtime.getRuntime().exec(new String[] { file.getAbsolutePath(), "sdk" });
} catch (IOException e1) {
e1.printStackTrace();
}
}
});
menu.add(item);
item = new JMenuItem("AVD Manager");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
File file = androidMode.getSDK().getAndroidTool();
PApplet.exec(new String[] { file.getAbsolutePath(), "avd" });
}
});
menu.add(item);
item = new JMenuItem("Reset ADB");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// editor.statusNotice("Resetting the Android Debug Bridge server.");
Devices.killAdbServer();
}
});
menu.add(item);
return menu;
}
/*
private void updateSdkMenu(final JMenu sdkMenu) {
try {
ArrayList targets = androidMode.getSDK().getAvailableSdkTargets();
if (targets.size() != 0) sdkMenu.removeAll();
AndroidSDK.SDKTarget lowestTargetAvailable = null;
JCheckBoxMenuItem lowestTargetMenuItem = null;
// String savedTargetVersion = Preferences.get("android.sdk.version");
String savedSdkName = Preferences.get("android.sdk.name");
boolean savedTargetSet = false;
for (final AndroidSDK.SDKTarget target : targets) {
if (target.version < 19) {
// TODO We do not support API level less than 19?
continue;
}
final JCheckBoxMenuItem item = new JCheckBoxMenuItem("API " + target.name + " (" + target.version + ")");
if (savedTargetSet == false && (lowestTargetAvailable == null || lowestTargetAvailable.version > target.version)) {
lowestTargetAvailable = target;
lowestTargetMenuItem = item;
}
if (target.name.equals(savedSdkName)) {
AndroidBuild.setSdkTarget(target, sketch);
item.setState(true);
savedTargetSet = true;
}
item.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (target.name.equals(AndroidBuild.target_sdk_version)) item.setState(true);
else item.setState(false);
}
});
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
AndroidBuild.setSdkTarget(target, sketch);
for (int i = 0; i < sdkMenu.getItemCount(); i++) {
((JCheckBoxMenuItem) sdkMenu.getItem(i)).setState(false);
}
item.setState(true);
}
});
sdkMenu.add(item);
}
if (!savedTargetSet && lowestTargetAvailable != null) {
AndroidBuild.setSdkTarget(lowestTargetAvailable, sketch);
lowestTargetMenuItem.setState(true);
}
} catch (IOException e) {
e.printStackTrace();
}
}
*/
private void setAppComponent(int opt) {
if (appComponent != opt) {
appComponent = opt;
resetManifest = true;
if (appComponent == AndroidBuild.FRAGMENT) {
settings.set("component", "app");
} else if (appComponent == AndroidBuild.WALLPAPER) {
settings.set("component", "wallpaper");
} else if (appComponent == AndroidBuild.WATCHFACE) {
settings.set("component", "watchface");
} else if (appComponent == AndroidBuild.CARDBOARD) {
settings.set("component", "cardboard");
}
settings.save();
}
}
/**
* Uses the main help menu, and adds a few extra options. If/when there's
* Android-specific documentation, we'll switch to that.
*/
public JMenu buildHelpMenu() {
JMenu menu = super.buildHelpMenu();
JMenuItem item;
menu.addSeparator();
item = new JMenuItem("Processing for Android Wiki");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Platform.openURL("http://wiki.processing.org/w/Android");
}
});
menu.add(item);
item = new JMenuItem("Android Developer Site");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Platform.openURL("http://developer.android.com/");
}
});
menu.add(item);
return menu;
}
/** override the standard grab reference to just show the java reference */
public void showReference(String filename) {
File javaReferenceFolder = Platform.getContentFile("modes/java/reference");
File file = new File(javaReferenceFolder, filename);
Platform.openURL(file.toURI().toString());
}
// protected void updateMode() {
// // When the selection is made, the menu will update itself
// boolean active = toggleItem.isSelected();
// if (active) {
// boolean rolling = true;
// if (sdk == null) {
// rolling = loadAndroid();
// }
// if (rolling) {
// editor.setHandlers(new RunHandler(), new PresentHandler(),
// new StopHandler(),
// new ExportHandler(), new ExportAppHandler());
// build = new AndroidBuild(editor, sdk);
// editor.statusNotice("Android mode enabled for this editor window.");
// }
// } else {
// editor.resetHandlers();
// editor.statusNotice("Android mode disabled.");
// }
// }
// protected boolean loadAndroid() {
// statusNotice("Loading Android tools.");
//
// try {
// sdk = AndroidSDK.find(this);
// } catch (final Exception e) {
// Base.showWarning("Android Tools Error", e.getMessage(), null);
// statusNotice("Android mode canceled.");
// return false;
// }
//
// // Make sure that the processing.android.core.* classes are available
// if (!checkCore()) {
// statusNotice("Android mode canceled.");
// return false;
// }
//
// statusNotice("Done loading Android tools.");
// return true;
// }
// static protected File getCoreZipLocation() {
// if (coreZipLocation == null) {
// coreZipLocation = checkCoreZipLocation();
// }
// return coreZipLocation;
// }
// private boolean checkCore() {
// final File target = getCoreZipLocation();
// if (!target.exists()) {
// try {
// final URL url = new URL(ANDROID_CORE_URL);
// PApplet.saveStream(target, url.openStream());
// } catch (final Exception e) {
// Base.showWarning("Download Error",
// "Could not download Android core.zip", e);
// return false;
// }
// }
// return true;
// }
@Override
public void dispose() {
if (updateDevicesTimer != null) {
updateDevicesTimer.cancel();
}
super.dispose();
}
public void statusError(String what) {
super.statusError(what);
// new Exception("deactivating RUN").printStackTrace();
// toolbar.deactivate(AndroidToolbar.RUN);
toolbar.deactivateRun();
}
public void sketchStopped() {
deactivateRun();
statusEmpty();
}
/**
* Build the sketch and run it inside an emulator with the debugger.
*/
public void handleRunEmulator() {
new Thread() {
public void run() {
// toolbar.activate(AndroidToolbar.RUN);
toolbar.activateRun();
startIndeterminate();
prepareRun();
try {
androidMode.handleRunEmulator(sketch, AndroidEditor.this, AndroidEditor.this,
resetManifest);
resetManifest = false;
} catch (SketchException e) {
statusError(e);
} catch (IOException e) {
statusError(e);
}
stopIndeterminate();
}
}.start();
}
/**
* Build the sketch and run it on a device with the debugger connected.
*/
public void handleRunDevice() {
if (Platform.isWindows() && !Preferences.getBoolean("android.warnings.usb_driver")) {
Preferences.setBoolean("android.warnings.usb_driver", true);
File sdkFolder = androidMode.getSDK().getSdkFolder();
String text = "";
File usbDriverFile = new File(sdkFolder, "extras/google/usb_driver");
if (usbDriverFile.exists()) {
text = USB_DRIVER_MESSAGEA + USB_DRIVER_MESSAGEB + usbDriverFile.getAbsolutePath();
} else {
text = USB_DRIVER_MESSAGEA;
}
AndroidMode.showMessage(USB_DRIVER_TITLE, text);
} else {
new Thread() {
public void run() {
toolbar.activateRun();
// toolbar.activate(AndroidToolbar.RUN);
startIndeterminate();
prepareRun();
try {
androidMode.handleRunDevice(sketch, AndroidEditor.this, AndroidEditor.this,
resetManifest);
resetManifest = false;
} catch (SketchException e) {
statusError(e);
} catch (IOException e) {
statusError(e);
}
stopIndeterminate();
}
}.start();
}
}
public void handleStop() {
toolbar.deactivateRun();
// toolbar.deactivate(AndroidToolbar.RUN);
stopIndeterminate();
androidMode.handleStop(this);
}
/**
* Create a release build of the sketch and have its apk files ready.
* If users want a debug build, they can do that from the command line.
*/
public void handleExportProject() {
if (handleExportCheckModified()) {
new Thread() {
public void run() {
// toolbar.activate(AndroidToolbar.EXPORT);
((AndroidToolbar) toolbar).activateExport();
startIndeterminate();
statusNotice("Exporting a debug version of the sketch...");
AndroidBuild build = new AndroidBuild(sketch, androidMode, appComponent, false);
try {
File exportFolder = build.exportProject();
if (exportFolder != null) {
Platform.openFolder(exportFolder);
statusNotice("Done with export.");
}
} catch (IOException e) {
statusError(e);
} catch (SketchException e) {
statusError(e);
}
stopIndeterminate();
// toolbar.deactivate(AndroidToolbar.EXPORT);
((AndroidToolbar)toolbar).deactivateExport();
}
}.start();
}
// try {
// buildReleaseForExport("debug");
// } catch (final MonitorCanceled ok) {
// statusNotice("Canceled.");
// } finally {
// deactivateExport();
// }
}
/**
* Create a release build of the sketch and install its apk files on the
* attached device.
*/
public void handleExportPackage() {
// Need to implement an entire signing setup first
// http://dev.processing.org/bugs/show_bug.cgi?id=1430
if (handleExportCheckModified()) {
// deactivateExport();
new KeyStoreManager(this);
}
}
public void startExportPackage(final String keyStorePassword) {
new Thread() {
public void run() {
startIndeterminate();
statusNotice("Exporting signed package...");
AndroidBuild build = new AndroidBuild(sketch, androidMode, appComponent, false);
try {
File projectFolder = build.exportPackage(keyStorePassword);
if (projectFolder != null) {
statusNotice("Done with export.");
Platform.openFolder(projectFolder);
} else {
statusError("Error with export");
}
} catch (IOException e) {
statusError(e);
} catch (SketchException e) {
statusError(e);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
stopIndeterminate();
}
}.start();
}
public int getAppComponent() {
return appComponent;
}
private void loadModeSettings() {
File sketchProps = new File(sketch.getCodeFolder(), "sketch.properties");
try {
settings = new Settings(sketchProps);
boolean save = false;
String component;
if (!sketchProps.exists()) {
component = AndroidBuild.DEFAULT_COMPONENT;
settings.set("component", component);
save = true;
} else {
component = settings.get("component");
if (component == null) {
component = AndroidBuild.DEFAULT_COMPONENT;
settings.set("component", component);
save = true;
}
}
if (save) settings.save();
if (component.equals("app")) {
appComponent = AndroidBuild.FRAGMENT;
fragmentItem.setState(true);
} else if (component.equals("wallpaper")) {
appComponent = AndroidBuild.WALLPAPER;
wallpaperItem.setState(true);
} else if (component.equals("watchface")) {
appComponent = AndroidBuild.WATCHFACE;
watchfaceItem.setState(true);
} else if (component.equals("cardboard")) {
appComponent = AndroidBuild.CARDBOARD;
cardboardItem.setState(true);
}
} catch (IOException e) {
System.err.println("While creating " + sketchProps + ": " + e.getMessage());
}
}
}