blob: 67de70971e06d7c8b5dbf85dbc118d2fffa2bd9e [file] [log] [blame]
/*
* 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.run;
import com.android.ddmlib.IDevice;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISystemImage;
import com.android.sdklib.OptionalLibrary;
import com.android.sdklib.devices.Abi;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Function;
import com.intellij.util.ThreeState;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class LaunchCompatibility {
@NonNls private static final String GOOGLE_APIS_TARGET_NAME = "Google APIs";
private final ThreeState myCompatible;
private final String myReason;
public static final LaunchCompatibility YES = new LaunchCompatibility(ThreeState.YES, null);
public LaunchCompatibility(ThreeState compatible, @Nullable String reason) {
myCompatible = compatible;
myReason = reason;
}
public LaunchCompatibility combine(@NotNull LaunchCompatibility other) {
if (myCompatible == ThreeState.NO) {
return this;
}
if (other.myCompatible == ThreeState.NO) {
return other;
}
if (myCompatible == ThreeState.UNSURE) {
return this;
}
return other;
}
public ThreeState isCompatible() {
return myCompatible;
}
@Nullable
public String getReason() {
return myReason;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("compatible", myCompatible).add("reason", myReason).toString();
}
@Override
public boolean equals(Object o) {
return o instanceof LaunchCompatibility &&
myCompatible == ((LaunchCompatibility)o).myCompatible &&
Objects.equal(myReason, ((LaunchCompatibility)o).myReason);
}
@Override
public int hashCode() {
return Objects.hashCode(myCompatible, myReason);
}
/**
* Returns whether an application with the given requirements can be run on the given device.
*
* @param minSdkVersion minSdkVersion specified by the application
* @param projectTarget android target corresponding to the targetSdkVersion
* @param requiredFeatures required list of hardware features
* @param device the device to check compatibility against
* @return a {@link ThreeState} indicating whether the application can be run on the device, and a reason if it isn't
* compatible.
*/
@NotNull
public static LaunchCompatibility canRunOnDevice(@NotNull AndroidVersion minSdkVersion,
@NotNull IAndroidTarget projectTarget,
@NotNull AndroidFacet facet,
Function<AndroidFacet, EnumSet<IDevice.HardwareFeature>> getRequiredHardwareFeatures,
@NotNull Set<String> supportedAbis,
@NotNull AndroidDevice device) {
// check if the device has the required minApi
// note that in cases where targetSdk is a preview platform, gradle sets minsdk to be the same as targetsdk,
// so as to only allow running on those systems
AndroidVersion deviceVersion = device.getVersion();
if (!deviceVersion.equals(AndroidVersion.DEFAULT) && !deviceVersion.canRun(minSdkVersion)) {
String reason = String.format("minSdk(%1$s) %3$s deviceSdk(%2$s)",
minSdkVersion,
deviceVersion,
minSdkVersion.getCodename() == null ? ">" : "!=");
return new LaunchCompatibility(ThreeState.NO, reason);
}
EnumSet<IDevice.HardwareFeature> requiredFeatures;
try {
requiredFeatures = getRequiredHardwareFeatures.fun(facet);
}
catch(IndexNotReadyException e) {
return new LaunchCompatibility(ThreeState.UNSURE, "Required features are unsure because indices are not ready.");
}
// check if the device provides the required features
for (IDevice.HardwareFeature feature : requiredFeatures) {
if (!device.supportsFeature(feature)) {
return new LaunchCompatibility(ThreeState.NO, "missing feature: " + feature);
}
}
// Typically, we only need to check that features required by the apk are supported by the device, which is done above
// In the case of watch though, we do an explicit check in the other direction: if the device is a watch, we don't want
// non-watch apks to be installed on it.
if (device.supportsFeature(IDevice.HardwareFeature.WATCH)) {
if (!requiredFeatures.contains(IDevice.HardwareFeature.WATCH)) {
return new LaunchCompatibility(ThreeState.NO, "missing uses-feature watch, non-watch apks cannot be launched on a watch");
}
}
// Verify that the device ABI matches one of the target ABIs for JNI apps.
if (!supportedAbis.isEmpty()) {
Set<String> deviceAbis = Sets.newLinkedHashSet();
for (Abi abi : device.getAbis()) {
deviceAbis.add(abi.toString());
}
if (Sets.intersection(supportedAbis, deviceAbis).isEmpty()) {
return new LaunchCompatibility(ThreeState.NO, "Device supports " + Joiner.on(", ").join(deviceAbis) +
", but APK only supports " + Joiner.on(", ").join(supportedAbis));
}
}
// we are done with checks for platform targets
if (projectTarget.isPlatform()) {
return YES;
}
// Add-ons specify a list of libraries. We need to check that the required libraries are available on the device.
// See AddOnTarget#canRunOn
List<OptionalLibrary> additionalLibs = projectTarget.getAdditionalLibraries();
if (additionalLibs.isEmpty()) {
return YES;
}
String targetName = projectTarget.getName();
if (GOOGLE_APIS_TARGET_NAME.equals(targetName)) {
// We'll assume that Google APIs are available on all devices.
return YES;
} else {
// Unsure because we don't have an easy way of determining whether those libraries are on a device
return new LaunchCompatibility(ThreeState.UNSURE, "unsure if device supports addon: " + targetName);
}
}
private static LaunchCompatibility isCompatibleAddonAvd(IAndroidTarget projectTarget, ISystemImage image) {
// validate that the vendor is the same for both the project and the avd
if (!StringUtil.equals(projectTarget.getVendor(), image.getAddonVendor().getDisplay())) {
String reason =
String.format("AVD vendor (%1$s) != AVD target (%2$s)", image.getAddonVendor().getDisplay(), projectTarget.getVendor());
return new LaunchCompatibility(ThreeState.NO, reason);
}
if (!StringUtil.equals(projectTarget.getName(), image.getTag().getDisplay())) {
String reason =
String.format("AVD target name (%1$s) != Project target name (%2$s)", image.getTag().getDisplay(), projectTarget.getName());
return new LaunchCompatibility(ThreeState.NO, reason);
}
return YES;
}
/** Returns whether a project with given minSdkVersion and target platform can be run on an AVD with given target platform. */
@NotNull
public static LaunchCompatibility canRunOnAvd(@NotNull AndroidVersion minSdkVersion,
@NotNull IAndroidTarget projectTarget,
@NotNull ISystemImage image) {
AndroidVersion avdVersion = image.getAndroidVersion();
if (!avdVersion.canRun(minSdkVersion)) {
String reason = String.format("minSdk(%1$s) %3$s deviceSdk(%2$s)",
minSdkVersion,
avdVersion,
minSdkVersion.getCodename() == null ? ">" : "!=");
return new LaunchCompatibility(ThreeState.NO, reason);
}
return projectTarget.isPlatform() ? YES : isCompatibleAddonAvd(projectTarget, image);
}
}