| /* |
| * 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.avdmanager; |
| |
| import com.android.resources.ScreenOrientation; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.ISystemImage; |
| import com.android.sdklib.devices.Device; |
| import com.android.sdklib.devices.DeviceManager; |
| 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.tools.idea.ddms.screenshot.DeviceArtDescriptor; |
| import com.android.tools.idea.wizard.dynamic.*; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.Maps; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.DialogWrapper; |
| import com.intellij.openapi.ui.Messages; |
| import com.intellij.openapi.util.io.FileUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.io.File; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import static com.android.tools.idea.avdmanager.AvdWizardConstants.*; |
| |
| /** |
| * Wizard for creating/editing AVDs |
| */ |
| public class AvdEditWizard extends DynamicWizard { |
| @Nullable private final AvdInfo myAvdInfo; |
| private final boolean myForceCreate; |
| private final JComponent myParent; |
| |
| public AvdEditWizard(@NotNull JComponent parent, |
| @Nullable Project project, |
| @Nullable Module module, |
| @Nullable AvdInfo avdInfo, |
| boolean forceCreate) { |
| super(project, module, "AvdEditWizard", new DialogWrapperHost(project, DialogWrapper.IdeModalityType.PROJECT)); |
| myAvdInfo = avdInfo; |
| myForceCreate = forceCreate; |
| myParent = parent; |
| setTitle("Virtual Device Configuration"); |
| } |
| |
| @Override |
| public void init() { |
| if (myAvdInfo != null) { |
| fillExistingInfo(myAvdInfo); |
| if (myForceCreate) { |
| String displayName = myAvdInfo.getProperties().get(AvdWizardConstants.DISPLAY_NAME_KEY.name); |
| getState().put(DISPLAY_NAME_KEY, String.format("Copy of %1$s", displayName)); |
| } |
| } |
| else { |
| initDefaultInfo(); |
| } |
| addPath(new AvdConfigurationPath(getDisposable())); |
| DynamicWizardStep configStep = new ConfigureAvdOptionsStep(getDisposable()); |
| addPath(new SingleStepPath(configStep)); |
| super.init(); |
| if (myForceCreate && myAvdInfo != null) { |
| getState().put(IS_IN_EDIT_MODE_KEY, false); |
| } |
| } |
| |
| /** |
| * Init the wizard with a set of reasonable defaults |
| */ |
| private void initDefaultInfo() { |
| ScopedStateStore state = getState(); |
| state.put(SCALE_SELECTION_KEY, DEFAULT_SCALE); |
| state.put(NETWORK_SPEED_KEY, DEFAULT_NETWORK_SPEED); |
| state.put(NETWORK_LATENCY_KEY, DEFAULT_NETWORK_LATENCY); |
| state.put(FRONT_CAMERA_KEY, DEFAULT_CAMERA); |
| state.put(BACK_CAMERA_KEY, DEFAULT_CAMERA); |
| state.put(INTERNAL_STORAGE_KEY, DEFAULT_INTERNAL_STORAGE); |
| state.put(IS_IN_EDIT_MODE_KEY, false); |
| state.put(USE_HOST_GPU_KEY, true); |
| state.put(DISPLAY_SD_SIZE_KEY, new Storage(100, Storage.Unit.MiB)); |
| state.put(DISPLAY_USE_EXTERNAL_SD_KEY, false); |
| } |
| |
| /** |
| * Init the wizard by filling in the information from the given AVD |
| */ |
| private void fillExistingInfo(@NotNull AvdInfo avdInfo) { |
| ScopedStateStore state = getState(); |
| List<Device> devices = DeviceManagerConnection.getDefaultDeviceManagerConnection().getDevices(); |
| Device selectedDevice = null; |
| String manufacturer = avdInfo.getDeviceManufacturer(); |
| String deviceId = avdInfo.getProperties().get(AvdManager.AVD_INI_DEVICE_NAME); |
| for (Device device : devices) { |
| if (manufacturer.equals(device.getManufacturer()) && deviceId.equals(device.getId())) { |
| selectedDevice = device; |
| break; |
| } |
| } |
| state.put(DEVICE_DEFINITION_KEY, selectedDevice); |
| IAndroidTarget target = avdInfo.getTarget(); |
| if (target != null) { |
| ISystemImage selectedImage = target.getSystemImage(avdInfo.getTag(), avdInfo.getAbiType()); |
| if (selectedImage != null) { |
| SystemImageDescription systemImageDescription = new SystemImageDescription(target, selectedImage); |
| state.put(SYSTEM_IMAGE_KEY, systemImageDescription); |
| } |
| } |
| |
| Map<String, String> properties = avdInfo.getProperties(); |
| |
| state.put(RAM_STORAGE_KEY, getStorageFromIni(properties.get(RAM_STORAGE_KEY.name))); |
| state.put(VM_HEAP_STORAGE_KEY, getStorageFromIni(properties.get(VM_HEAP_STORAGE_KEY.name))); |
| state.put(INTERNAL_STORAGE_KEY, getStorageFromIni(properties.get(INTERNAL_STORAGE_KEY.name))); |
| |
| String sdCardLocation = null; |
| if (properties.get(EXISTING_SD_LOCATION.name) != null) { |
| sdCardLocation = properties.get(EXISTING_SD_LOCATION.name); |
| } |
| else if (properties.get(SD_CARD_STORAGE_KEY.name) != null) { |
| sdCardLocation = FileUtil.join(avdInfo.getDataFolderPath(), "sdcard.img"); |
| } |
| state.put(EXISTING_SD_LOCATION, sdCardLocation); |
| String dataFolderPath = avdInfo.getDataFolderPath(); |
| File sdLocationFile = null; |
| if (sdCardLocation != null) { |
| sdLocationFile = new File(sdCardLocation); |
| } |
| if (sdLocationFile != null && Objects.equal(sdLocationFile.getParent(), dataFolderPath)) { |
| // the image is in the AVD folder, consider it to be internal |
| File sdFile = new File(sdCardLocation); |
| Storage sdCardSize = new Storage(sdFile.length()); |
| myState.put(DISPLAY_USE_EXTERNAL_SD_KEY, false); |
| myState.put(SD_CARD_STORAGE_KEY, sdCardSize); |
| myState.put(DISPLAY_SD_SIZE_KEY, sdCardSize); |
| } |
| else { |
| // the image is external |
| myState.put(DISPLAY_USE_EXTERNAL_SD_KEY, true); |
| myState.put(DISPLAY_SD_LOCATION_KEY, sdCardLocation); |
| } |
| |
| String scale = properties.get(SCALE_SELECTION_KEY.name); |
| if (scale != null) { |
| state.put(SCALE_SELECTION_KEY, AvdScaleFactor.findByValue(scale)); |
| } |
| state.put(USE_HOST_GPU_KEY, fromIniString(properties.get(USE_HOST_GPU_KEY.name))); |
| state.put(USE_SNAPSHOT_KEY, fromIniString(properties.get(USE_SNAPSHOT_KEY.name))); |
| state.put(FRONT_CAMERA_KEY, properties.get(FRONT_CAMERA_KEY.name)); |
| state.put(BACK_CAMERA_KEY, properties.get(BACK_CAMERA_KEY.name)); |
| state.put(NETWORK_LATENCY_KEY, properties.get(NETWORK_LATENCY_KEY.name)); |
| state.put(NETWORK_SPEED_KEY, properties.get(NETWORK_SPEED_KEY.name)); |
| state.put(HAS_HARDWARE_KEYBOARD_KEY, fromIniString(properties.get(HAS_HARDWARE_KEYBOARD_KEY.name))); |
| state.put(DISPLAY_NAME_KEY, AvdManagerConnection.getAvdDisplayName(avdInfo)); |
| |
| String orientation = properties.get(HardwareProperties.HW_INITIAL_ORIENTATION); |
| if (orientation != null) { |
| state.put(DEFAULT_ORIENTATION_KEY, ScreenOrientation.getByShortDisplayName(orientation)); |
| } |
| |
| String skinPath = properties.get(CUSTOM_SKIN_FILE_KEY.name); |
| if (skinPath != null) { |
| File skinFile; |
| if (skinPath.equals(NO_SKIN.getPath())) { |
| skinFile = NO_SKIN; |
| } |
| else { |
| skinFile = new File(skinPath); |
| } |
| if (skinFile.isDirectory()) { |
| state.put(CUSTOM_SKIN_FILE_KEY, skinFile); |
| } |
| } |
| String backupSkinPath = properties.get(BACKUP_SKIN_FILE_KEY.name); |
| if (backupSkinPath != null) { |
| File skinFile = new File(backupSkinPath); |
| if (skinFile.isDirectory() || FileUtil.filesEqual(skinFile, NO_SKIN)) { |
| state.put(BACKUP_SKIN_FILE_KEY, skinFile); |
| } |
| } |
| state.put(IS_IN_EDIT_MODE_KEY, true); |
| } |
| |
| /** |
| * Decodes the given string from the INI file and returns a {@link Storage} of |
| * corresponding size. |
| */ |
| @Nullable |
| private static Storage getStorageFromIni(String iniString) { |
| if (iniString == null) { |
| return null; |
| } |
| String numString = iniString.substring(0, iniString.length() - 1); |
| char unitChar = iniString.charAt(iniString.length() - 1); |
| Storage.Unit selectedUnit = null; |
| for (Storage.Unit u : Storage.Unit.values()) { |
| if (u.toString().charAt(0) == unitChar) { |
| selectedUnit = u; |
| break; |
| } |
| } |
| if (selectedUnit == null) { |
| selectedUnit = Storage.Unit.MiB; // Values expressed without a unit read as MB |
| numString = iniString; |
| } |
| try { |
| long numLong = Long.parseLong(numString); |
| return new Storage(numLong, selectedUnit); |
| } |
| catch (NumberFormatException e) { |
| return null; |
| } |
| } |
| |
| @Override |
| public void performFinishingActions() { |
| Device device = myState.get(DEVICE_DEFINITION_KEY); |
| assert device != null; // Validation should be done by individual steps |
| SystemImageDescription systemImageDescription = myState.get(SYSTEM_IMAGE_KEY); |
| assert systemImageDescription != null; |
| ScreenOrientation orientation = myState.get(DEFAULT_ORIENTATION_KEY); |
| if (orientation == null) { |
| orientation = device.getDefaultState().getOrientation(); |
| } |
| |
| Map<String, String> hardwareProperties = DeviceManager.getHardwareProperties(device); |
| Map<String, Object> userEditedProperties = myState.flatten(); |
| |
| // Remove the SD card setting that we're not using |
| String sdCard = null; |
| |
| Boolean useExternalSdCard = myState.get(DISPLAY_USE_EXTERNAL_SD_KEY); |
| boolean useExisting = useExternalSdCard != null && useExternalSdCard; |
| if (!useExisting) { |
| if (Objects.equal(myState.get(SD_CARD_STORAGE_KEY), myState.get(DISPLAY_SD_SIZE_KEY))) { |
| // unchanged, use existing card |
| useExisting = true; |
| } |
| } |
| boolean hasSdCard; |
| if (!useExisting) { |
| userEditedProperties.remove(EXISTING_SD_LOCATION.name); |
| Storage storage = myState.get(DISPLAY_SD_SIZE_KEY); |
| myState.put(SD_CARD_STORAGE_KEY, storage); |
| if (storage != null) { |
| sdCard = toIniString(storage, false); |
| } |
| hasSdCard = storage != null && storage.getSize() > 0; |
| } |
| else { |
| sdCard = myState.get(DISPLAY_SD_LOCATION_KEY); |
| myState.put(EXISTING_SD_LOCATION, sdCard); |
| userEditedProperties.remove(SD_CARD_STORAGE_KEY.name); |
| assert sdCard != null; |
| hasSdCard = true; |
| hardwareProperties.put(HardwareProperties.HW_SDCARD, toIniString(true)); |
| } |
| hardwareProperties.put(HardwareProperties.HW_SDCARD, toIniString(hasSdCard)); |
| // Remove any internal keys from the map |
| userEditedProperties = Maps.filterEntries(userEditedProperties, new Predicate<Map.Entry<String, Object>>() { |
| @Override |
| public boolean apply(Map.Entry<String, Object> input) { |
| return !input.getKey().startsWith(WIZARD_ONLY) && input.getValue() != null; |
| } |
| }); |
| // Call toString() on all remaining values |
| hardwareProperties.putAll(Maps.transformEntries(userEditedProperties, new Maps.EntryTransformer<String, Object, String>() { |
| @Override |
| public String transformEntry(String key, Object value) { |
| if (value instanceof Storage) { |
| if (key.equals(AvdWizardConstants.RAM_STORAGE_KEY.name) || key.equals(AvdWizardConstants.VM_HEAP_STORAGE_KEY.name)) { |
| return toIniString((Storage)value, true); |
| } |
| else { |
| return toIniString((Storage)value, false); |
| } |
| } |
| else if (value instanceof Boolean) { |
| return toIniString((Boolean)value); |
| } |
| else if (value instanceof AvdScaleFactor) { |
| return toIniString((AvdScaleFactor)value); |
| } |
| else if (value instanceof File) { |
| return toIniString((File)value); |
| } |
| else if (value instanceof Double) { |
| return toIniString((Double)value); |
| } |
| else { |
| return value.toString(); |
| } |
| } |
| })); |
| |
| File skinFile = myState.get(CUSTOM_SKIN_FILE_KEY); |
| if (skinFile == null) { |
| skinFile = resolveSkinPath(device.getDefaultHardware().getSkinFile(), systemImageDescription); |
| } |
| File backupSkinFile = myState.get(BACKUP_SKIN_FILE_KEY); |
| if (backupSkinFile != null) { |
| hardwareProperties.put(AvdManager.AVD_INI_BACKUP_SKIN_PATH, backupSkinFile.getPath()); |
| } |
| |
| // Add defaults if they aren't already set differently |
| if (!hardwareProperties.containsKey(AvdManager.AVD_INI_SKIN_DYNAMIC)) { |
| hardwareProperties.put(AvdManager.AVD_INI_SKIN_DYNAMIC, toIniString(true)); |
| } |
| if (!hardwareProperties.containsKey(HardwareProperties.HW_KEYBOARD)) { |
| hardwareProperties.put(HardwareProperties.HW_KEYBOARD, toIniString(false)); |
| } |
| |
| boolean isCircular = device.isScreenRound(); |
| |
| String tempAvdName = myState.get(AvdWizardConstants.AVD_ID_KEY); |
| if (tempAvdName == null || tempAvdName.isEmpty()) { |
| tempAvdName = calculateAvdName(myAvdInfo, hardwareProperties, device, myForceCreate); |
| } |
| final String avdName = tempAvdName; |
| |
| // If we're editing an AVD and we downgrade a system image, wipe the user data with confirmation |
| if (myAvdInfo != null && !myForceCreate) { |
| IAndroidTarget target = myAvdInfo.getTarget(); |
| if (target != null) { |
| |
| int oldApiLevel = target.getVersion().getFeatureLevel(); |
| int newApiLevel = systemImageDescription.getVersion().getFeatureLevel(); |
| final String oldApiName = target.getVersion().getApiString(); |
| final String newApiName = systemImageDescription.getVersion().getApiString(); |
| if (oldApiLevel > newApiLevel || |
| (oldApiLevel == newApiLevel && target.getVersion().isPreview() && !systemImageDescription.getVersion().isPreview())) { |
| final AtomicReference<Boolean> shouldContinue = new AtomicReference<Boolean>(); |
| ApplicationManager.getApplication().invokeAndWait(new Runnable() { |
| @Override |
| public void run() { |
| String message = |
| String.format(Locale.getDefault(), "You are about to downgrade %1$s from API level %2$s to API level %3$s.\n" + |
| "This requires a wipe of the userdata partition of the AVD.\nDo you wish to " + |
| "continue with the data wipe?", avdName, oldApiName, newApiName); |
| int result = Messages.showYesNoDialog((Project)null, message, "Confirm Data Wipe", AllIcons.General.QuestionDialog); |
| shouldContinue.set(result == Messages.YES); |
| } |
| }, ModalityState.any()); |
| if (shouldContinue.get()) { |
| AvdManagerConnection.getDefaultAvdManagerConnection().wipeUserData(myAvdInfo); |
| } |
| else { |
| return; |
| } |
| } |
| } |
| } |
| |
| AvdManagerConnection connection = AvdManagerConnection.getDefaultAvdManagerConnection(); |
| connection.createOrUpdateAvd(myForceCreate ? null : myAvdInfo, avdName, device, systemImageDescription, orientation, isCircular, sdCard, |
| skinFile, hardwareProperties, false); |
| } |
| |
| @NotNull |
| @Override |
| protected String getProgressTitle() { |
| return "Saving AVD..."; |
| } |
| |
| @Nullable |
| @Override |
| public JComponent getProgressParentComponent() { |
| return myParent; |
| } |
| |
| /** |
| * Resolve a possibly relative path into a skin directory. If {@code image} is provided, try to match the given path |
| * against a skin path from {@code image.getSkins()}. If no match is found or no image is provided, look in the path given by |
| * {@link DeviceArtDescriptor#getBundledDescriptorsFolder()}. If no match is found, return {@code path}. |
| * |
| * @param path The path to resolve. |
| * @param image A SystemImageDescription to use as an additional source of skin directories. |
| * @return The resolved path. |
| */ |
| @Nullable |
| public static File resolveSkinPath(@Nullable File path, @Nullable SystemImageDescription image) { |
| if (path == null || path.getPath().isEmpty()) { |
| return path; |
| } |
| if (path.equals(NO_SKIN)) { |
| return NO_SKIN; |
| } |
| if (!path.isAbsolute()) { |
| if (image != null) { |
| File[] skins = image.getSkins(); |
| for (File skin : skins) { |
| if (skin.getPath().endsWith(File.separator + path.getPath())) { |
| return skin; |
| } |
| } |
| } |
| File resourceDir = DeviceArtDescriptor.getBundledDescriptorsFolder(); |
| if (resourceDir != null) { |
| File resourcePath = new File(resourceDir, path.getPath()); |
| if (resourcePath.exists()) { |
| return resourcePath; |
| } |
| } |
| } |
| return path; |
| } |
| |
| @NotNull |
| private static String toIniString(@NotNull Double value) { |
| return String.format(Locale.US, "%f", value); |
| } |
| |
| @NotNull |
| private static String toIniString(@NotNull File value) { |
| return value.getPath(); |
| } |
| |
| /** |
| * Encode the given value as a string that can be placed in the AVD's INI file. |
| */ |
| @NotNull |
| private static String toIniString(@NotNull AvdScaleFactor value) { |
| return value.getValue(); |
| } |
| |
| @NotNull |
| private static String calculateAvdName(@Nullable AvdInfo avdInfo, |
| @NotNull Map<String, String> hardwareProperties, |
| @NotNull Device device, |
| boolean forceCreate) { |
| if (avdInfo != null && !forceCreate) { |
| return avdInfo.getName(); |
| } |
| String candidateBase = hardwareProperties.get(AvdManagerConnection.AVD_INI_DISPLAY_NAME); |
| if (candidateBase == null || candidateBase.isEmpty()) { |
| String deviceName = device.getDisplayName().replace(' ', '_'); |
| String manufacturer = device.getManufacturer().replace(' ', '_'); |
| candidateBase = String.format("AVD_for_%1$s_by_%2$s", deviceName, manufacturer); |
| } |
| return cleanAvdName(AvdManagerConnection.getDefaultAvdManagerConnection(), candidateBase, true); |
| } |
| |
| /** |
| * Get a version of {@code candidateBase} modified such that it is a valid filename. Invalid characters will be |
| * removed, and if requested the name will be made unique. |
| * |
| * @param candidateBase the name on which to base the avd name. |
| * @param uniquify if true, _n will be appended to the name if necessary to make the name unique, where n is the first |
| * number that makes the filename unique. |
| * @return The modified filename. |
| */ |
| public static String cleanAvdName(@NotNull AvdManagerConnection connection, @NotNull String candidateBase, boolean uniquify) { |
| candidateBase = candidateBase.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_"); |
| if (candidateBase.isEmpty()) { |
| candidateBase = "myavd"; |
| } |
| String candidate = candidateBase; |
| if (uniquify) { |
| int i = 1; |
| while (connection.avdExists(candidate)) { |
| candidate = String.format("%1$s_%2$d", candidateBase, i++); |
| } |
| } |
| return candidate; |
| } |
| |
| /** |
| * Encode the given value as a string that can be placed in the AVD's INI file. |
| * Example: 10M or 1G |
| */ |
| @NotNull |
| public static String toIniString(@NotNull Storage storage, boolean convertToMb) { |
| Storage.Unit unit = convertToMb ? Storage.Unit.MiB : storage.getAppropriateUnits(); |
| String unitString = convertToMb ? "" : unit.toString().substring(0, 1); |
| return String.format("%1$d%2$s", storage.getSizeAsUnit(unit), unitString); |
| } |
| |
| /** |
| * Encode the given value as a string that can be placed in the AVD's INI file. |
| */ |
| @NotNull |
| private static String toIniString(@NotNull Boolean b) { |
| return b ? "yes" : "no"; |
| } |
| |
| private static boolean fromIniString(@Nullable String s) { |
| return "yes".equals(s); |
| } |
| |
| @Override |
| protected String getWizardActionDescription() { |
| return "Create/Edit an Android Virtual Device"; |
| } |
| } |