由于我的应用特有的原因,我想在两个不同的productFlavor中使用两个不同版本的Google Play服务库。但gradle给了我熟悉的错误信息:
请通过更新google-services插件的版本(https://bintray.com/android/android-tools/com.google.gms.google-services/提供有关最新版本的信息)或将com.google.android.gms的版本更新为10.2.4来修复版本冲突。
通常我会通过使用一致版本的GPS库来解决这个问题。但是,在这种情况下,我认为不一致是可以的,因为我正在将应用程序编译成两种不同的风格。这不太合适:
app build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '25.0.2'
defaultConfig {
applicationId "com.albertcbraun.googleplayservicesversionconflicttestcase"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
flavor1 {
applicationId 'com.albertcbraun.flavor1'
}
flavor2 {
applicationId 'com.albertcbraun.flavor2'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
// these are the problematic lines. GPS versions differ:
flavor1Compile 'com.google.android.gms:play-services-identity:10.2.4'
flavor2Compile 'com.google.android.gms:play-services-identity:9.6.1'
}
apply plugin: 'com.google.gms.google-services'
项目build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.google.gms:google-services:3.0.0'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Gradle版本: 3.3
Android Studio版本: 2.3.1
答案 0 :(得分:0)
没有gradle不支持同一个库的多个版本。它将选择最新的,Gradle默认使用最新的冲突版本。但是,您可以更改此行为。使用此方法将解决方案配置为在任何版本冲突上急切失败,例如:同一个配置中的多个不同版本的相同依赖项(组和名称相同)。
来自此处的来源https://gradle.org/docs/current/dsl/org.gradle.api.artifacts.ResolutionStrategy.html
答案 1 :(得分:0)
========已修订04/05/2017 ========
这是实验性的,但是,FWIW,我能够破解“味道意识”到版本3.0.0的GoogleServicesTask.java和GoogleServicesPlugin.groovy(它构成了GoogleServicesPlugin for gradle)。
原始插件通过检查build.gradle中的“compile”语句(在名为findTargetVersion的方法中)推断出 GPS库版本。但我改变了。有了这个hack,您可以在扩展属性中为每个flavor预先指定这些版本。
这种方法既没有经过充分测试也没有生产就绪,但它能够编译具有两种不同产品风格的两个不同版本的GPS库。另请注意:Android Studio会抱怨您有两个不同的版本(红色下划线),但AS仍然可以让您在buildvariants中选择任何一种风格并实际执行构建。 (至少,它确实适合我。)
首先在同一个build.gradle中添加这两个扩展值(或任何你想要使用的版本),在某个地方合理:
ext.flavor1GPSVersion = "10.2.1"
ext.flavor2GPSVersion = "10.2.4"
其次,在应用模块的build.gradle中注释掉或删除此“apply”行:
apply plugin: GoogleServicesPlugin
最后,直接将以下修改版本的GoogleServicesTask.java和GoogleServicesPlugin.groovy粘贴到该build.gradle文件的底部(并记住在底部包含新的“apply plugin”行):
// ************************************************************//
// ********** Multi Flavor Google Services Plugin *************//
// ************************************************************//
import org.gradle.api.tasks.Optional;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.io.Files;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
class MultiFlavorGoogleServicesPlugin implements Plugin<Project> {
public final static String JSON_FILE_NAME = 'google-services.json'
public final static String MODULE_GROUP = "com.google.android.gms"
public final static String MODULE_GROUP_FIREBASE = "com.google.firebase"
public final static String MODULE_CORE = "firebase-core"
public final static String MINIMUM_VERSION = "9.0.0"
private static final String TAG = "GoogleServicesPlugin";
@Override
void apply(Project project) {
if (project.plugins.hasPlugin("android") ||
project.plugins.hasPlugin("com.android.application")) {
// this is a bit fragile but since this is internal usage this is ok
// (another plugin could declare itself to be 'android')
for (def flavor : project.android.productFlavors) {
addDependency(project, flavor.name)
}
setupPlugin(project, false)
return
}
if (project.plugins.hasPlugin("android-library") ||
project.plugins.hasPlugin("com.android.library")) {
// this is a bit fragile but since this is internal usage this is ok
// (another plugin could declare itself to be 'android-library')
for (def flavor : project.android.productFlavors) {
addDependency(project, flavor.name)
}
setupPlugin(project, true)
return
}
// If the google-service plugin is applied before any android plugin.
// We should warn that google service plugin should be applied at
// the bottom of build file.
showWarningForPluginLocation(project)
// Setup google-services plugin after android plugin is applied.
project.plugins.withId("android", {
setupPlugin(project, false)
})
project.plugins.withId("android-library", {
setupPlugin(project, true)
})
// Add dependencies after the build file is evaluate and hopefully it
// can be execute before android plugin process the dependencies.
for (def flavor : project.android.productFlavors) {
project.afterEvaluate({
addDependency(project, flavor.name)
})
}
}
private static void showWarningForPluginLocation(Project project) {
project.getLogger().warn(
"please apply google-services plugin at the bottom of the build file.")
}
private static boolean checkMinimumVersion(Project project, String flavorName) {
String[] subTargetVersions = findTargetVersion(project, flavorName).split("\\.") //targetVersion.split("\\.")
String[] subMinimumVersions = MINIMUM_VERSION.split("\\.")
for (int i = 0; i < subTargetVersions.length && i < subMinimumVersions.length; i++) {
Integer subTargetVersion = Integer.valueOf(subTargetVersions[i])
Integer subMinimumVersion = Integer.valueOf(subMinimumVersions[i])
if (subTargetVersion > subMinimumVersion) {
return true;
} else if (subTargetVersion < subMinimumVersion) {
return false;
}
}
return subTargetVersions.length >= subMinimumVersions.length;
}
private void addDependency(Project project, String flavorName) {
//targetVersion = findTargetVersion(project).split("-")[0]
if (checkMinimumVersion(project, flavorName)) {
// If the target version is not lower than the minimum version
project.dependencies.add('compile', MODULE_GROUP_FIREBASE + ':' + MODULE_CORE + ':' + findTargetVersion(project, flavorName).split("-")[0])
} else {
throw new GradleException("Version: " + targetVersion + " is lower than the minimum version (" +
MINIMUM_VERSION + ") required for google-services plugin.")
}
}
private static String findTargetVersion(Project project, String flavorName) {
return project.ext[flavorName + "GPSVersion"];
}
private void setupPlugin(Project project, boolean isLibrary) {
if (isLibrary) {
project.android.libraryVariants.all { variant ->
handleVariant(project, variant)
}
} else {
project.android.applicationVariants.all { variant ->
handleVariant(project, variant)
}
}
}
private static void handleVariant(Project project,
def variant) {
File quickstartFile = null
String variantName = "$variant.dirName";
String[] variantTokens = variantName.split('/')
List<String> fileLocation = new ArrayList<>()
FlavorAwareGoogleServicesTask task = project.tasks
.create("process${variant.name.capitalize()}GoogleServices",
FlavorAwareGoogleServicesTask)
if (variantTokens.length == 2) {
// If flavor and buildType are found.
String flavorName = variantTokens[0]
String buildType = variantTokens[1]
fileLocation.add('src/' + flavorName + '/' + buildType)
fileLocation.add('src/' + buildType + '/' + flavorName)
fileLocation.add('src/' + flavorName)
fileLocation.add('src/' + buildType)
task.moduleVersion = findTargetVersion(project, flavorName)
task.flavorName = flavorName;
} else if (variantTokens.length == 1) {
// If only buildType is found.
fileLocation.add('src/' + variantTokens[0])
}
String searchedLocation = System.lineSeparator()
for (String location : fileLocation) {
File jsonFile = project.file(location + '/' + JSON_FILE_NAME)
searchedLocation = searchedLocation + jsonFile.getPath() + System.lineSeparator()
if (jsonFile.isFile()) {
quickstartFile = jsonFile
break
}
}
if (quickstartFile == null) {
quickstartFile = project.file(JSON_FILE_NAME)
searchedLocation = searchedLocation + quickstartFile.getPath()
}
File outputDir =
project.file("$project.buildDir/generated/res/google-services/$variant.dirName")
task.quickstartFile = quickstartFile
task.intermediateDir = outputDir
task.packageName = variant.applicationId
task.moduleGroup = MODULE_GROUP
// Use the target version for the task.
//task.moduleVersion = targetVersion;
variant.registerResGeneratingTask(task, outputDir)
task.searchedLocation = searchedLocation
}
}
/**
* Helper task for plugin
* */
public class FlavorAwareGoogleServicesTask extends DefaultTask {
private static final String STATUS_DISABLED = "1";
private static final String STATUS_ENABLED = "2";
private static final String OAUTH_CLIENT_TYPE_WEB = "3";
/**
* The input is not technically optional but we want to control the error message.
* Without @Optional, Gradle will complain itself the file is missing.
*/
@InputFile @Optional
public File quickstartFile;
@OutputDirectory
public File intermediateDir;
@Input
public String packageName;
@Input
public String moduleGroup;
@Input
public String moduleVersion;
@Input
public String searchedLocation;
@Input
public String flavorName;
@TaskAction
public void action() throws IOException {
checkVersionConflict();
if (!quickstartFile.isFile()) {
throw new GradleException(String.format("File %s is missing. " +
"The Google Services Plugin cannot function without it. %n Searched Location: %s",
quickstartFile.getName(), searchedLocation));
}
getProject().getLogger().warn("Parsing json file: " + quickstartFile.getPath());
// delete content of outputdir.
deleteFolder(intermediateDir);
if (!intermediateDir.mkdirs()) {
throw new GradleException("Failed to create folder: " + intermediateDir);
}
JsonElement root = new JsonParser().parse(Files.newReader(quickstartFile, Charsets.UTF_8));
if (!root.isJsonObject()) {
throw new GradleException("Malformed root json");
}
JsonObject rootObject = root.getAsJsonObject();
Map<String, String> resValues = new TreeMap<String, String>();
Map<String, Map<String, String>> resAttributes = new TreeMap<String, Map<String, String>>();
handleProjectNumberAndProjectId(rootObject, resValues);
handleFirebaseUrl(rootObject, resValues);
JsonObject clientObject = getClientForPackageName(rootObject);
if (clientObject != null) {
handleAnalytics(clientObject, resValues);
handleMapsService(clientObject, resValues);
handleGoogleApiKey(clientObject, resValues);
handleGoogleAppId(clientObject, resValues);
handleWebClientId(clientObject, resValues);
} else {
throw new GradleException("No matching client found for package name '" + packageName + "'");
}
// write the values file.
File values = new File(intermediateDir, "values");
if (!values.exists() && !values.mkdirs()) {
throw new GradleException("Failed to create folder: " + values);
}
Files.write(getValuesContent(resValues, resAttributes), new File(values, "values.xml"), Charsets.UTF_8);
}
/**
* Check if there is any conflict between Play-Services Version
*/
private void checkVersionConflict() {
Project project = getProject();
ConfigurationContainer configurations = project.getConfigurations();
if (configurations == null) {
return;
}
boolean hasConflict = false;
for (Configuration configuration : configurations) {
if (configuration == null) {
continue;
}
if (configuration.name.startsWith(flavorName + "Compile")) {
DependencySet dependencies = configuration.getDependencies();
if (dependencies == null) {
continue;
}
for (Dependency dependency : dependencies) {
if (dependency == null || dependency.getGroup() == null || dependency.getVersion() == null) {
continue;
}
println("checkVersionConflict for flavor:" + flavorName +
" comparing moduleGroup:" + moduleGroup + " to " + dependency.getGroup() +
" moduleVersion:" + moduleVersion + " to " + dependency.getVersion());
if (dependency.getGroup().equals(moduleGroup)
&& !dependency.getVersion().equals(moduleVersion)) {
hasConflict = true;
project.getLogger().warn("Found " + dependency.getGroup() + ":" +
dependency.getName() + ":" + dependency.getVersion() + ", but version " +
moduleVersion + " is needed for the google-services plugin.");
}
}
}
}
if (hasConflict) {
throw new GradleException("Please fix the version conflict either by updating the version " +
"of the google-services plugin (information about the latest version is available at " +
"https://bintray.com/android/android-tools/com.google.gms.google-services/) or updating " +
"the version of " + moduleGroup + " to " + moduleVersion + ".");
}
}
private void handleFirebaseUrl(JsonObject rootObject, Map<String, String> resValues)
throws IOException {
JsonObject projectInfo = rootObject.getAsJsonObject("project_info");
if (projectInfo == null) {
throw new GradleException("Missing project_info object");
}
JsonPrimitive firebaseUrl = projectInfo.getAsJsonPrimitive("firebase_url");
if (firebaseUrl != null) {
resValues.put("firebase_database_url", firebaseUrl.getAsString());
}
}
/**
* Handle project_info/project_number for @string/gcm_defaultSenderId, and fill the res map with the read value.
* @param rootObject the root Json object.
* @throws IOException
*/
private void handleProjectNumberAndProjectId(JsonObject rootObject, Map<String, String> resValues)
throws IOException {
JsonObject projectInfo = rootObject.getAsJsonObject("project_info");
if (projectInfo == null) {
throw new GradleException("Missing project_info object");
}
JsonPrimitive projectNumber = projectInfo.getAsJsonPrimitive("project_number");
if (projectNumber == null) {
throw new GradleException("Missing project_info/project_number object");
}
resValues.put("gcm_defaultSenderId", projectNumber.getAsString());
JsonPrimitive bucketName = projectInfo.getAsJsonPrimitive("storage_bucket");
if (bucketName != null) {
resValues.put("google_storage_bucket", bucketName.getAsString());
}
}
private void handleWebClientId(JsonObject clientObject, Map<String, String> resValues) {
JsonArray array = clientObject.getAsJsonArray("oauth_client");
if (array != null) {
final int count = array.size();
for (int i = 0 ; i < count ; i++) {
JsonElement oauthClientElement = array.get(i);
if (oauthClientElement == null || !oauthClientElement.isJsonObject()) {
continue;
}
JsonObject oauthClientObject = oauthClientElement.getAsJsonObject();
JsonPrimitive clientType = oauthClientObject.getAsJsonPrimitive("client_type");
if (clientType == null) {
continue;
}
String clientTypeStr = clientType.getAsString();
if (!OAUTH_CLIENT_TYPE_WEB.equals(clientTypeStr)) {
continue;
}
JsonPrimitive clientId = oauthClientObject.getAsJsonPrimitive("client_id");
if (clientId == null) {
continue;
}
resValues.put("default_web_client_id", clientId.getAsString());
return;
}
}
}
/**
* Handle a client object for analytics (@xml/global_tracker)
* @param clientObject the client Json object.
* @throws IOException
*/
private void handleAnalytics(JsonObject clientObject, Map<String, String> resValues)
throws IOException {
JsonObject analyticsService = getServiceByName(clientObject, "analytics_service");
if (analyticsService == null) return;
JsonObject analyticsProp = analyticsService.getAsJsonObject("analytics_property");
if (analyticsProp == null) return;
JsonPrimitive trackingId = analyticsProp.getAsJsonPrimitive("tracking_id");
if (trackingId == null) return;
resValues.put("ga_trackingId", trackingId.getAsString());
File xml = new File(intermediateDir, "xml");
if (!xml.exists() && !xml.mkdirs()) {
throw new GradleException("Failed to create folder: " + xml);
}
Files.write(getGlobalTrackerContent(
trackingId.getAsString()),
new File(xml, "global_tracker.xml"),
Charsets.UTF_8);
}
/**
* Handle a client object for maps (@string/google_maps_key).
* @param clientObject the client Json object.
* @throws IOException
*/
private void handleMapsService(JsonObject clientObject, Map<String, String> resValues)
throws IOException {
JsonObject mapsService = getServiceByName(clientObject, "maps_service");
if (mapsService == null) return;
String apiKey = getAndroidApiKey(clientObject);
if (apiKey != null) {
resValues.put("google_maps_key", apiKey);
return;
}
throw new GradleException("Missing api_key/current_key object");
}
private void handleGoogleApiKey(JsonObject clientObject, Map<String, String> resValues) {
String apiKey = getAndroidApiKey(clientObject);
if (apiKey != null) {
resValues.put("google_api_key", apiKey);
// TODO: remove this once SDK starts to use google_api_key.
resValues.put("google_crash_reporting_api_key", apiKey);
return;
}
// if google_crash_reporting_api_key is missing.
// throw new GradleException("Missing api_key/current_key object");
throw new GradleException("Missing api_key/current_key object");
}
private String getAndroidApiKey(JsonObject clientObject) {
JsonArray array = clientObject.getAsJsonArray("api_key");
if (array != null) {
final int count = array.size();
for (int i = 0 ; i < count ; i++) {
JsonElement apiKeyElement = array.get(i);
if (apiKeyElement == null || !apiKeyElement.isJsonObject()) {
continue;
}
JsonObject apiKeyObject = apiKeyElement.getAsJsonObject();
JsonPrimitive currentKey = apiKeyObject.getAsJsonPrimitive("current_key");
if (currentKey == null) {
continue;
}
return currentKey.getAsString();
}
}
return null;
}
/**
* find an item in the "client" array that match the package name of the app
* @param jsonObject the root json object.
* @return a JsonObject representing the client entry or null if no match is found.
*/
private JsonObject getClientForPackageName(JsonObject jsonObject) {
JsonArray array = jsonObject.getAsJsonArray("client");
if (array != null) {
final int count = array.size();
for (int i = 0 ; i < count ; i++) {
JsonElement clientElement = array.get(i);
if (clientElement == null || !clientElement.isJsonObject()) {
continue;
}
JsonObject clientObject = clientElement.getAsJsonObject();
JsonObject clientInfo = clientObject.getAsJsonObject("client_info");
if (clientInfo == null) continue;
JsonObject androidClientInfo = clientInfo.getAsJsonObject("android_client_info");
if (androidClientInfo == null) continue;
JsonPrimitive clientPackageName = androidClientInfo.getAsJsonPrimitive("package_name");
if (clientPackageName == null) continue;
if (packageName.equals(clientPackageName.getAsString())) {
return clientObject;
}
}
}
return null;
}
/**
* Handle a client object for Google App Id.
*/
private void handleGoogleAppId(JsonObject clientObject, Map<String, String> resValues)
throws IOException {
JsonObject clientInfo = clientObject.getAsJsonObject("client_info");
if (clientInfo == null) {
// Should not happen
throw new GradleException("Client does not have client info");
}
JsonPrimitive googleAppId = clientInfo.getAsJsonPrimitive("mobilesdk_app_id");
if (googleAppId == null) return;
String googleAppIdStr = googleAppId.getAsString();
if (Strings.isNullOrEmpty(googleAppIdStr)) return;
resValues.put("google_app_id", googleAppIdStr);
}
/**
* Finds a service by name in the client object. Returns null if the service is not found
* or if the service is disabled.
*
* @param clientObject the json object that represents the client.
* @param serviceName the service name
* @return the service if found.
*/
private JsonObject getServiceByName(JsonObject clientObject, String serviceName) {
JsonObject services = clientObject.getAsJsonObject("services");
if (services == null) return null;
JsonObject service = services.getAsJsonObject(serviceName);
if (service == null) return null;
JsonPrimitive status = service.getAsJsonPrimitive("status");
if (status == null) return null;
String statusStr = status.getAsString();
if (STATUS_DISABLED.equals(statusStr)) return null;
if (!STATUS_ENABLED.equals(statusStr)) {
getLogger().warn(String.format("Status with value '%1$s' for service '%2$s' is unknown",
statusStr,
serviceName));
return null;
}
return service;
}
private static String getGlobalTrackerContent(String ga_trackingId) {
return "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<resources>\n" +
" <string name=\"ga_trackingId\" translatable=\"false\">" + ga_trackingId + "</string>\n" +
"</resources>\n";
}
private static String getValuesContent(Map<String, String> values,
Map<String, Map<String, String>> attributes) {
StringBuilder sb = new StringBuilder(256);
sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<resources>\n");
for (Map.Entry<String, String> entry : values.entrySet()) {
String name = entry.getKey();
sb.append(" <string name=\"").append(name).append("\" translatable=\"false\"");
if (attributes.containsKey(name)) {
for (Map.Entry<String, String> attr : attributes.get(name).entrySet()) {
sb.append(" ").append(attr.getKey()).append("=\"")
.append(attr.getValue()).append("\"");
}
}
sb.append(">").append(entry.getValue()).append("</string>\n");
}
sb.append("</resources>\n");
return sb.toString();
}
private static void deleteFolder(final File folder) {
if (!folder.exists()) {
return;
}
File[] files = folder.listFiles();
if (files != null) {
for (final File file : files) {
if (file.isDirectory()) {
deleteFolder(file);
} else {
if (!file.delete()) {
throw new GradleException("Failed to delete: " + file);
}
}
}
}
if (!folder.delete()) {
throw new GradleException("Failed to delete: " + folder);
}
}
}
apply plugin: MultiFlavorGoogleServicesPlugin
======== ORIGINAL 04/04/2017 =======
进一步研究,我发现google-services插件是错误消息的来源以及只能使用一个版本的GPS库依赖关系的约束。 (如果你在上面的build.gradle中注释掉 apply plugin:'com.google.gms.google-services',那么错误信息就不会发生。但是,你真的需要这个插件,所以评论它不是一个解决方案。)
为了做到这一点,您需要创建一个google-services插件的修改版本(应用于上面的app build.gradle底部)。
google-services插件包含两个文件:GoogleServicesTask.java和GoogleServicesPlugin.groovy。 (这些可以在gradle home的'caches'子区域深处找到)。
似乎GoogleServicesTask.java坚持使用它找到的第一个版本的GPS库(在方法findTargetVersion中)。
(已编辑以保存字符)