/*
* 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.tools.idea.welcome;
import com.android.sdklib.devices.Storage;
import com.android.tools.idea.wizard.ScopedStateStore;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Hashtable;
/**
* Wizard page for setting up IntelĀ® HAXM settings for Mac OS X platform
*/
public final class MacEmulatorSettingsStep extends FirstRunWizardStep {
private static final int MAJOR_TICKS = 4;
private static final int MINOR_TICKS = 512;
// Smallest adjustment user will be able to make with a slider (if the RAM size is small enough)
private static final int MAX_TICK_RESOLUTION = 32; //Mb
private static final int MIN_EMULATOR_MEMORY = 512; //Mb
private static final Logger LOG = Logger.getInstance(MacEmulatorSettingsStep.class);
private final ScopedStateStore.Key<Integer> myKeyEmulatorMemory;
private final int myRecommendedMemorySize;
private JPanel myRoot;
private JButton myIntelHAXMDocumentationButton;
private JSlider myMemorySlider;
private JSpinner myMemorySize;
private JLabel myUnitLabel;
private JButton myRecommended;
public MacEmulatorSettingsStep(ScopedStateStore.Key<Integer> keyEmulatorMemory) {
super("Emulator Settings");
myUnitLabel.setText(SetupEmulatorPath.UI_UNITS.toString());
myKeyEmulatorMemory = keyEmulatorMemory;
final long memorySize = getMemorySize();
WelcomeUIUtils.makeButtonAHyperlink(myIntelHAXMDocumentationButton, SetupEmulatorPath.HAXM_URL);
myRecommendedMemorySize = setupSliderAndSpinner(memorySize, myMemorySlider, myMemorySize);
setComponent(myRoot);
myRecommended.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
myState.put(myKeyEmulatorMemory, myRecommendedMemorySize);
}
});
}
@SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"})
private static int setupSliderAndSpinner(long memorySize, JSlider slider, JSpinner spinner) {
int recommendedMemorySize = getRecommendedMemoryAllocation(memorySize);
int maxMemory = Math.max(getMaxMemoryAllocation(memorySize), recommendedMemorySize);
int ticks = Math.min(maxMemory / MAX_TICK_RESOLUTION, MINOR_TICKS);
// Empty border is needed to avoid clipping long first and/or last label
slider.setBorder(BorderFactory.createEmptyBorder(0, 30, 0, 30));
slider.setMinimum(MIN_EMULATOR_MEMORY);
slider.setMaximum(maxMemory);
slider.setMinorTickSpacing(maxMemory / ticks);
slider.setMajorTickSpacing(maxMemory / MAJOR_TICKS);
Hashtable labels = new Hashtable();
int totalMemory = (int)(memorySize / SetupEmulatorPath.UI_UNITS.getNumberOfBytes());
int labelSpacing = totalMemory / MAJOR_TICKS;
// Avoid overlapping
int minDistanceBetweenLabels = labelSpacing / 4;
for (int i = maxMemory; i >= labelSpacing; i -= labelSpacing) {
if (Math.abs(i - recommendedMemorySize) > minDistanceBetweenLabels) {
labels.put(i, new JLabel(getMemoryLabel(i)));
}
}
if (recommendedMemorySize > minDistanceBetweenLabels) {
labels.put(MIN_EMULATOR_MEMORY, new JLabel(getMemoryLabel(MIN_EMULATOR_MEMORY)));
}
labels.put(recommendedMemorySize, createRecommendedSizeLabel(recommendedMemorySize));
slider.setLabelTable(labels);
spinner.setModel(new SpinnerNumberModel(MIN_EMULATOR_MEMORY, MIN_EMULATOR_MEMORY, maxMemory, maxMemory / ticks));
return recommendedMemorySize;
}
private static JComponent createRecommendedSizeLabel(int memorySize) {
String labelText = String.format("<html><center>%s<br>(Recommended)<center></html>", getMemoryLabel(memorySize));
final Font boldLabelFont = UIUtil.getLabelFont().deriveFont(Font.BOLD);
// This is the only way as JSlider resets label font.
return new JLabel(labelText) {
@Override
public Font getFont() {
return boldLabelFont;
}
};
}
private static int getMaxMemoryAllocation(long memorySize) {
final long GB = Storage.Unit.GiB.getNumberOfBytes();
final long maxMemory;
if (memorySize > 4 * GB) {
maxMemory = memorySize - 2 * GB;
}
else {
maxMemory = memorySize / 2;
}
return (int)(maxMemory / SetupEmulatorPath.UI_UNITS.getNumberOfBytes());
}
private static int getRecommendedMemoryAllocation(long memorySize) {
final long GB = Storage.Unit.GiB.getNumberOfBytes();
final long defaultMemory;
if (memorySize > 4 * GB) {
defaultMemory = 2 * GB;
}
else {
if (memorySize > 2 * GB) {
defaultMemory = GB;
}
else {
defaultMemory = GB / 2;
}
}
return (int)(defaultMemory / SetupEmulatorPath.UI_UNITS.getNumberOfBytes());
}
private static String getMemoryLabel(int memorySize) {
return new Storage(memorySize * SetupEmulatorPath.UI_UNITS.getNumberOfBytes()).toString();
}
private static long getMemorySize() {
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
// This is specific to JDKs derived from Oracle JDK (including OpenJDK and Apple JDK among others).
// Other then this, there's no standard way of getting memory size
// without adding 3rd party libraries or using native code.
try {
Class<?> oracleSpecificMXBean = Class.forName("com.sun.management.OperatingSystemMXBean");
Method getPhysicalMemorySizeMethod = oracleSpecificMXBean.getMethod("getTotalPhysicalMemorySize");
Object result = getPhysicalMemorySizeMethod.invoke(osMXBean);
if (result instanceof Number) {
return ((Number)result).longValue();
}
}
catch (ClassNotFoundException e) {
// Unsupported JDK
}
catch (NoSuchMethodException e) {
// Unsupported JDK
}
catch (InvocationTargetException e) {
LOG.error(e);
}
catch (IllegalAccessException e) {
LOG.error(e);
}
// Maximum memory allocatable to emulator - 32G. Only used if non-Oracle JRE.
return 32L * (1 << 30);
}
@Override
public boolean isStepVisible() {
return SystemInfo.isMac;
}
@Override
public void init() {
myState.put(myKeyEmulatorMemory, myRecommendedMemorySize);
register(myKeyEmulatorMemory, myMemorySlider);
register(myKeyEmulatorMemory, myMemorySize, new SpinnerBinding());
}
@Nullable
@Override
public JLabel getMessageLabel() {
return null;
}
@Override
public JComponent getPreferredFocusedComponent() {
return myMemorySlider;
}
private static class SpinnerBinding extends ComponentBinding<Integer, JSpinner> {
@Override
public void setValue(@Nullable Integer newValue, @NotNull JSpinner component) {
component.setValue(newValue);
}
@Nullable
@Override
public Integer getValue(@NotNull JSpinner component) {
return (Integer)component.getValue();
}
@Override
public void addChangeListener(@NotNull ChangeListener listener, @NotNull JSpinner component) {
component.addChangeListener(listener);
}
}
}