| /* |
| * Copyright (C) 2015 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 static com.android.sdklib.AndroidVersion.MAX_32_BIT_API; |
| import static com.android.sdklib.AndroidVersion.MIN_4K_TV_API; |
| import static com.android.sdklib.AndroidVersion.MIN_FOLDABLE_DEVICE_API; |
| import static com.android.sdklib.AndroidVersion.MIN_FREEFORM_DEVICE_API; |
| import static com.android.sdklib.AndroidVersion.MIN_HINGE_FOLDABLE_DEVICE_API; |
| import static com.android.sdklib.AndroidVersion.MIN_PIXEL_4A_DEVICE_API; |
| import static com.android.sdklib.AndroidVersion.MIN_RECTANGULAR_WEAR_API; |
| import static com.android.sdklib.AndroidVersion.MIN_RESIZABLE_DEVICE_API; |
| import static com.android.sdklib.AndroidVersion.MIN_RECOMMENDED_API; |
| import static com.android.sdklib.AndroidVersion.MIN_RECOMMENDED_WEAR_API; |
| |
| import com.android.repository.Revision; |
| import com.android.resources.ScreenOrientation; |
| import com.android.sdklib.AndroidVersion; |
| import com.android.sdklib.devices.Abi; |
| import com.android.sdklib.devices.Device; |
| import com.android.sdklib.repository.IdDisplay; |
| import com.android.sdklib.repository.targets.SystemImage; |
| import com.android.tools.adtui.util.FormScalingUtil; |
| import com.android.tools.analytics.CommonMetricsData; |
| import com.android.tools.idea.avdmanager.SystemImagePreview.ImageRecommendation; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.Lists; |
| import com.google.wireless.android.sdk.stats.ProductDetails; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.ui.JBColor; |
| import com.intellij.ui.components.JBLabel; |
| import com.intellij.ui.components.JBTabbedPane; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.system.CpuArch; |
| import com.intellij.util.ui.AsyncProcessIcon; |
| import com.intellij.util.ui.ListTableModel; |
| import java.awt.BorderLayout; |
| import java.awt.Dimension; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.util.List; |
| import javax.swing.BorderFactory; |
| import javax.swing.JButton; |
| import javax.swing.JPanel; |
| import javax.swing.RowFilter; |
| import javax.swing.event.ChangeEvent; |
| import javax.swing.event.ChangeListener; |
| import org.jetbrains.android.util.AndroidBundle; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| /** |
| * UI panel that presents the user with a list of {@link SystemImageDescription}s to choose from. |
| * |
| * You should register a listener via {@link #addSystemImageListener(Consumer)} to be notified of |
| * when the user updates their choice. |
| */ |
| public class ChooseSystemImagePanel extends JPanel |
| implements SystemImageList.SystemImageSelectionListener, SystemImageListModel.StatusIndicator, Disposable { |
| |
| private static final boolean IS_ARM64_HOST_OS = CpuArch.isArm64() || |
| CommonMetricsData.getOsArchitecture() == ProductDetails.CpuArchitecture.X86_ON_ARM; |
| |
| private final List<Consumer<SystemImageDescription>> mySystemImageListeners = Lists.newArrayListWithExpectedSize(1); |
| |
| private JPanel myPanel; |
| private SystemImageList myRecommendedImageList; |
| private SystemImageList myPerformantImageList; |
| private SystemImageList myOtherImageList; |
| private SystemImagePreview mySystemImagePreview; |
| private JBTabbedPane myTabPane; |
| private JBLabel myStatusLabel; |
| private JButton myRefreshButton; |
| private AsyncProcessIcon myAsyncIcon; |
| private SystemImageListModel myListModel; |
| |
| @Nullable private Device myDevice; |
| @Nullable private SystemImageDescription mySystemImage; |
| |
| public void setDevice(@Nullable Device device) { |
| myDevice = device; |
| myListModel.refreshImages(false); |
| } |
| |
| public ChooseSystemImagePanel(@Nullable Project project, @Nullable Device initialDevice, @Nullable SystemImageDescription initialSystemImage) { |
| super(new BorderLayout()); |
| FormScalingUtil.scaleComponentTree(this.getClass(), myPanel); |
| |
| mySystemImage = initialSystemImage; |
| myDevice = initialDevice; |
| |
| myListModel = new SystemImageListModel(project, this); |
| setupImageLists(); |
| myRefreshButton.setIcon(AllIcons.Actions.Refresh); |
| myRefreshButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| myListModel.refreshImages(true); |
| } |
| }); |
| |
| // The center panel contains performant system images that are not recommended, therefore depending on the host system architecture, we |
| // either provide x86 or ARM based images. |
| //noinspection DialogTitleCapitalization |
| myTabPane.setTitleAt(1, IS_ARM64_HOST_OS |
| ? AndroidBundle.message("avd.manager.arm.images") |
| : AndroidBundle.message("avd.manager.x86.images")); |
| |
| myTabPane.addChangeListener(new ChangeListener() { |
| @Override |
| public void stateChanged(ChangeEvent changeEvent) { |
| previewCurrentTab(); |
| } |
| }); |
| |
| myRecommendedImageList.addSelectionListener(this); |
| myRecommendedImageList.setBorder(BorderFactory.createLineBorder(JBColor.lightGray)); |
| |
| myListModel.refreshLocalImagesSynchronously(); |
| myListModel.refreshImages(false); |
| setSelectedImage(mySystemImage); |
| |
| add(myPanel); |
| } |
| |
| @NotNull |
| @VisibleForTesting |
| static SystemImageClassification getClassificationForDevice(@NotNull SystemImageDescription image, @Nullable Device theDevice, |
| boolean isArm64HostOs) { |
| |
| Abi abi = Abi.getEnum(image.getAbiType()); |
| SystemImageClassification classification = getClassificationFromParts(abi, |
| image.getVersion(), |
| image.getTag(), |
| isArm64HostOs); |
| |
| if (theDevice != null && !image.getTag().equals(SystemImage.WEAR_TAG)) { |
| // For non-Wear devices, adjust the recommendation based on Play Store |
| if (theDevice.hasPlayStore()) { |
| // The device supports Google Play Store. Recommend only system images that also support Play Store. |
| if (classification == SystemImageClassification.RECOMMENDED && !image.getSystemImage().hasPlayStore()) { |
| classification = (abi == Abi.X86 || abi == Abi.X86_64) ? SystemImageClassification.PERFORMANT : SystemImageClassification.OTHER; |
| } |
| } |
| else { |
| // The device does not support Google Play Store. Hide Play Store system images. |
| if (image.getSystemImage().hasPlayStore()) { |
| classification = SystemImageClassification.FORBIDDEN; |
| } |
| } |
| } |
| return classification; |
| } |
| |
| @NotNull |
| private SystemImageClassification getClassification(@NotNull SystemImageDescription image) { |
| return getClassificationForDevice(image, myDevice, IS_ARM64_HOST_OS); |
| } |
| |
| @NotNull |
| @VisibleForTesting |
| static SystemImageClassification getClassificationFromParts(@NotNull Abi abi, |
| @NotNull AndroidVersion androidVersion, |
| @NotNull IdDisplay tag, |
| boolean isArm64HostOs) { |
| int apiLevel = androidVersion.getApiLevel(); |
| boolean isAvdIntel = abi == Abi.X86 || abi == Abi.X86_64; |
| boolean isAvdArm = abi == Abi.ARM64_V8A; |
| |
| if (!isAvdArm && !isAvdIntel) { |
| // None of these system images run natively on supported Android Studio platforms. |
| return SystemImageClassification.OTHER; |
| } |
| |
| if (!androidVersion.isBaseExtension() && androidVersion.getExtensionLevel() != null) { |
| // System images that contain extension levels but are not the base SDK should not be placed in RECOMMENDED tab, it should be either |
| // PERFORMANT or OTHER. |
| return isArm64HostOs == isAvdIntel ? SystemImageClassification.OTHER : SystemImageClassification.PERFORMANT; |
| } |
| |
| if (isAvdIntel == isArm64HostOs) { |
| return SystemImageClassification.OTHER; |
| } |
| |
| if (tag.equals(SystemImage.WEAR_TAG)) { |
| // For Wear, recommend based on API level (all Wear have Google APIs) |
| return (apiLevel >= MIN_RECOMMENDED_WEAR_API && (isArm64HostOs || isAvdIntel)) |
| ? SystemImageClassification.RECOMMENDED |
| : SystemImageClassification.PERFORMANT; |
| } |
| if (apiLevel < MIN_RECOMMENDED_API) { |
| return SystemImageClassification.PERFORMANT; |
| } |
| if (SystemImageDescription.TAGS_WITH_GOOGLE_API.contains(tag) && |
| (isArm64HostOs || |
| SystemImageDescription.TV_TAGS.contains(tag) || // Android TV does not ship x86_64 images at any API level. |
| (apiLevel <= MAX_32_BIT_API && abi == Abi.X86) || |
| (apiLevel > MAX_32_BIT_API && abi == Abi.X86_64)) |
| ) { |
| // Only recommend ARM images on ARM hosts, 32-bit x86 system images where api-level <= 30, and 64-bit images where api-level > 30. |
| return SystemImageClassification.RECOMMENDED; |
| } |
| return SystemImageClassification.PERFORMANT; |
| } |
| |
| public static boolean systemImageMatchesDevice(@Nullable SystemImageDescription image, @Nullable Device device) { |
| if (device == null || image == null) { |
| return false; |
| } |
| |
| String deviceTagId = device.getTagId(); |
| String deviceId = device.getId(); |
| IdDisplay imageTag = image.getTag(); |
| |
| // Foldable device requires Q preview or API29 and above. |
| if (device.getDefaultHardware().getScreen().isFoldable() && |
| image.getVersion().getFeatureLevel() < MIN_FOLDABLE_DEVICE_API) { |
| return false; |
| } |
| |
| // Freeform display device requires R preview DP2 or API30 and above. |
| if (deviceId.equals("13.5in Freeform")) { |
| if (image.getVersion() == null || image.getVersion().getFeatureLevel() < MIN_FREEFORM_DEVICE_API) { |
| return false; |
| } |
| if ("R".equals(image.getVersion().getCodename())) { |
| if (image.getRevision() == null || image.getRevision().compareTo(new Revision(2, 0, 0)) <= 0) { |
| return false; |
| } |
| } |
| } |
| |
| // hinge foldable device requires API30 and above |
| if (deviceId.equals("7.6in Foldable") || |
| deviceId.equals("8in Foldable") || |
| deviceId.equals("6.7in Foldable") || |
| deviceId.equals("7.4in Rollable")) { |
| if (image.getVersion() == null || image.getVersion().getFeatureLevel() < MIN_HINGE_FOLDABLE_DEVICE_API) { |
| return false; |
| } |
| } |
| |
| // pixel 4a requires API30 and above |
| if (deviceId.equals(("pixel_4a"))) { |
| if (image.getVersion() == null || image.getVersion().getFeatureLevel() < MIN_PIXEL_4A_DEVICE_API) { |
| return false; |
| } |
| } |
| |
| // Resizable requires API31 and above |
| if (deviceId.equals(("resizable"))) { |
| if (image.getVersion() == null || image.getVersion().getFeatureLevel() < MIN_RESIZABLE_DEVICE_API) { |
| return false; |
| } |
| } |
| |
| // Unknown/generic device? |
| if (deviceTagId == null || deviceTagId.equals(SystemImage.DEFAULT_TAG.getId())) { |
| // If so include all system images, except those we *know* not to match this type |
| // of device. Rather than just checking "imageTag.getId().equals(SystemImage.DEFAULT_TAG.getId())" |
| // here (which will filter out system images with a non-default tag, such as the Google API |
| // system images (see issue #78947), we instead deliberately skip the other form factor images |
| return !imageTag.equals(SystemImage.ANDROID_TV_TAG) && !imageTag.equals(SystemImage.GOOGLE_TV_TAG) && |
| !imageTag.equals(SystemImage.WEAR_TAG) && !imageTag.equals(SystemImage.DESKTOP_TAG) && |
| !imageTag.equals(SystemImage.CHROMEOS_TAG) && !imageTag.equals(SystemImage.AUTOMOTIVE_TAG) && |
| !imageTag.equals(SystemImage.AUTOMOTIVE_PLAY_STORE_TAG); |
| } |
| |
| // 4K TV requires at least S (API 31) |
| if (deviceId.equals("tv_4k")) { |
| if (image.getVersion() == null || image.getVersion().getFeatureLevel() < MIN_4K_TV_API) { |
| return false; |
| } |
| } |
| |
| // Android TV / Google TV and vice versa |
| if (deviceTagId.equals(SystemImage.ANDROID_TV_TAG.getId()) || deviceTagId.equals(SystemImage.GOOGLE_TV_TAG.getId())) { |
| return imageTag.equals(SystemImage.ANDROID_TV_TAG) || imageTag.equals(SystemImage.GOOGLE_TV_TAG); |
| } |
| |
| // Non-square rectangular Wear OS requires at least P (API 28) |
| if (imageTag.equals(SystemImage.WEAR_TAG) && !device.isScreenRound()) { |
| Dimension screenSize = device.getScreenSize(ScreenOrientation.PORTRAIT); |
| if (screenSize != null && screenSize.getWidth() != screenSize.getHeight()) { |
| if (image.getVersion().getFeatureLevel() < MIN_RECTANGULAR_WEAR_API) { |
| return false; |
| } |
| } |
| } |
| |
| return deviceTagId.equals(imageTag.getId()); |
| } |
| |
| private void setupImageLists() { |
| setupImageList(myRecommendedImageList); |
| setupImageList(myPerformantImageList); |
| setupImageList(myOtherImageList); |
| setImageListFilters(); |
| } |
| |
| private void setupImageList(@NotNull SystemImageList list) { |
| list.setModel(myListModel); |
| list.addSelectionListener(this); |
| list.setBorder(BorderFactory.createLineBorder(JBColor.lightGray)); |
| } |
| |
| private void setImageListFilters() { |
| myRecommendedImageList.setRowFilter(new ClassificationRowFilter(SystemImageClassification.RECOMMENDED)); |
| myPerformantImageList.setRowFilter(new ClassificationRowFilter(SystemImageClassification.PERFORMANT)); |
| myOtherImageList.setRowFilter(new ClassificationRowFilter(SystemImageClassification.OTHER)); |
| } |
| |
| @Override |
| public void onSystemImageSelected(@Nullable SystemImageDescription systemImage) { |
| mySystemImagePreview.setImage(systemImage); |
| if (systemImage != null && !systemImage.isRemote()) { |
| mySystemImage = systemImage; |
| } |
| else { |
| mySystemImage = null; |
| } |
| |
| for (Consumer<SystemImageDescription> listener : mySystemImageListeners) { |
| listener.consume(mySystemImage); |
| } |
| } |
| |
| private void setSelectedImage(@Nullable SystemImageDescription systemImage) { |
| if (systemImage != null) { |
| SystemImageClassification classification = getClassification(systemImage); |
| switch (classification) { |
| case RECOMMENDED: |
| myRecommendedImageList.setSelectedImage(systemImage); |
| myTabPane.setSelectedIndex(0); |
| break; |
| case PERFORMANT: |
| myPerformantImageList.setSelectedImage(systemImage); |
| myTabPane.setSelectedIndex(1); |
| break; |
| default: |
| myOtherImageList.setSelectedImage(systemImage); |
| myTabPane.setSelectedIndex(2); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void onRefreshStart(@NotNull String message) { |
| myStatusLabel.setText(message); |
| myRefreshButton.setEnabled(false); |
| myAsyncIcon.setVisible(true); |
| } |
| |
| @Override |
| public void onRefreshDone(@NotNull String message, boolean partlyDownloaded) { |
| myStatusLabel.setText(message); |
| myRefreshButton.setEnabled(true); |
| myAsyncIcon.setVisible(false); |
| myRecommendedImageList.restoreSelection(partlyDownloaded, mySystemImage); |
| myPerformantImageList.restoreSelection(partlyDownloaded, mySystemImage); |
| myOtherImageList.restoreSelection(partlyDownloaded, mySystemImage); |
| previewCurrentTab(); |
| } |
| |
| private void previewCurrentTab() { |
| switch (myTabPane.getSelectedIndex()) { |
| case 0: // "Recommended" |
| myRecommendedImageList.makeListCurrent(); |
| if (myDevice != null && SystemImage.WEAR_TAG.getId().equals(myDevice.getTagId())) { |
| mySystemImagePreview.showExplanationForRecommended(ImageRecommendation.RECOMMENDATION_WEAR); |
| } else if (myDevice != null && myDevice.hasPlayStore()) { |
| mySystemImagePreview.showExplanationForRecommended(ImageRecommendation.RECOMMENDATION_GOOGLE_PLAY); |
| } else { |
| mySystemImagePreview.showExplanationForRecommended(ImageRecommendation.RECOMMENDATION_X86); |
| } |
| break; |
| case 1: // Performant images, title either "x86 Images" or "ARM Images" |
| myPerformantImageList.makeListCurrent(); |
| mySystemImagePreview.showExplanationForRecommended(ImageRecommendation.RECOMMENDATION_NONE); |
| break; |
| default: // "Other images" |
| myOtherImageList.makeListCurrent(); |
| mySystemImagePreview.showExplanationForRecommended(ImageRecommendation.RECOMMENDATION_NONE); |
| break; |
| } |
| } |
| |
| private void createUIComponents() { |
| myAsyncIcon = new AsyncProcessIcon("refresh images"); |
| myRecommendedImageList = new SystemImageList(); |
| myPerformantImageList = new SystemImageList(); |
| myOtherImageList = new SystemImageList(); |
| mySystemImagePreview = new SystemImagePreview(this); |
| } |
| |
| public void addSystemImageListener(@NotNull Consumer<SystemImageDescription> onSystemImageSelected) { |
| mySystemImageListeners.add(onSystemImageSelected); |
| onSystemImageSelected.consume(mySystemImage); |
| } |
| |
| @Override |
| public void dispose() { |
| mySystemImageListeners.clear(); |
| } |
| |
| @Nullable |
| public SystemImageDescription getSystemImage() { |
| return mySystemImage; |
| } |
| |
| @VisibleForTesting |
| enum SystemImageClassification { |
| RECOMMENDED, |
| PERFORMANT, |
| OTHER, |
| FORBIDDEN |
| } |
| |
| private class ClassificationRowFilter extends RowFilter<ListTableModel<SystemImageDescription>, Integer> { |
| private final SystemImageClassification myClassification; |
| |
| public ClassificationRowFilter(@NotNull SystemImageClassification classification) { |
| myClassification = classification; |
| } |
| |
| @Override |
| public boolean include(Entry<? extends ListTableModel<SystemImageDescription>, ? extends Integer> entry) { |
| SystemImageDescription image = myListModel.getRowValue(entry.getIdentifier()); |
| return getClassification(image) == myClassification && |
| systemImageMatchesDevice(image, myDevice) && |
| versionSupported(image); |
| } |
| |
| private boolean versionSupported(@NotNull SystemImageDescription image) { |
| return image.getVersion().getApiLevel() > 2; |
| } |
| } |
| } |