Использование типов сборки в Gradle для запуска того же приложения, которое использует ContentProvider на одном устройстве


Я настроил Gradle, чтобы добавить суффикс имени пакета в мое отладочное приложение, чтобы у меня была версия выпуска, которую я использую, и отладочная версия на одном телефоне. Я ссылался на это: http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Types

моя сборка.файл Gradle выглядит так:

...
android
{
    ...
    buildTypes
    {
        debug
        {
            packageNameSuffix ".debug"
            versionNameSuffix " debug"
        }
    }
}

все работает нормально, пока я не начну использовать ContentProvider в моем приложении. Я получаю:

Failure [INSTALL_FAILED_CONFLICTING_PROVIDER]

Я понимаю, что это происходит потому что два приложения (release и debug) регистрируют одни и те же полномочия ContentProvider.

Я вижу одну возможность решить эту проблему. Если я правильно понимаю, вы должны быть в состоянии указать различные файлы для использования при построении. Затем я должен быть в состоянии поместить разные полномочия в разные файлы ресурсов (и из Манифеста установить полномочия как строковый ресурс) и сказать Gradle использовать другой ресурс для отладки сборки. Это возможно? Если да, то любые подсказки о том, как этого добиться, будут потрясающе!

или, может быть, можно напрямую изменить манифест с помощью Gradle? Любое другое решение о том, как запустить то же приложение с ContentProvider на одном устройстве всегда приветствуется.

14 115

14 ответов:

ни один из существующих ответов меня не удовлетворил, однако свобода была близка. Так вот как я это делаю. Прежде всего на данный момент я работаю с:

  • Android Studio Beta 0.8.2
  • Gradle плагин 0.12.+
  • ш 1.12

мой цель бежать Debug версия вместе с Release версия на том же устройстве с использованием того же ContentProvider.


на построить.gradle of ваше приложение устанавливает суффикс для отладки сборки:

buildTypes {
    debug {
        applicationIdSuffix ".debug"
    }
}

на AndroidManifest.xml

новая система сборки Android совет: ContentProvider переименование полномочий

Я думаю, все вы слышали о новой системе сборки на базе Android Gradle. Давайте будем честными, эта новая система сборки-это огромный шаг вперед по сравнению с предыдущей. Это еще не окончательное (на момент написания этой статьи, последняя версия 0.4.2), но вы уже можете использовать его безопасно в большинстве ваших проектов.

Я лично переключил большую часть своего проекта на эту новую систему сборки и были некоторые проблемы из-за отсутствия поддержки в определенных ситуациях. Одним из которых является поддержка переименования полномочий ContentProvider

новая встроенная система Android позволяет вам работать с различными типами вашего приложения, просто изменяя имя пакета во время сборки. Одним из главных преимуществ этого улучшения является то, что теперь вы можете одновременно устанавливать две разные версии вашего приложения на одном устройстве. Например:

android {
   compileSdkVersion 17
   buildToolsVersion "17.0.0"

   defaultConfig {
       packageName "com.cyrilmottier.android.app"
       versionCode 1
       versionName "1"
       minSdkVersion 14 // Listen to +Jeff Gilfelt advices :)
       targetSdkVersion 17
   }

   buildTypes {
       debug {
        packageNameSuffix ".debug"
            versionNameSuffix "-debug"
       }
   }
}

используя такое Gradle в конфигурация, вы можете собрать два различных APKs:

* отладка APK с com.сирильмотье.андроид.приложение.имя пакета отладки * Релиз APK с com.сирильмотье.андроид.имя пакета приложения

единственная проблема заключается в том, что вы не сможете установить два APKs одновременно, если они оба предоставляют ContentProvider с теми же полномочиями. Довольно логично нам нужно переименовать полномочия в зависимости от текущего типа сборки ... но это не поддерживается Система сборки Gradle (пока? ... Я уверен, что это будет исправлено в ближайшее время). Итак, вот способ пойти:

Сначала нам нужно переместить объявление поставщика Android manifest ContentProvider в соответствующий тип сборки. Для этого у нас будет просто :

src / debug / AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.cyrilmottier.android.app"
   android:versionCode="1"
   android:versionName="1">

   <application>

       <provider
           android:name=".provider.Provider1"
           android:authorities="com.cyrilmottier.android.app.debug.provider"
           android:exported="false" />

   </application>
</manifest>

src / release / AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.cyrilmottier.android.app"
   android:versionCode="1"
   android:versionName="1">

   <application>

       <provider
           android:name=".provider.Provider1"
           android:authorities="com.cyrilmottier.android.app.provider"
           android:exported="false" />

   </application>
</manifest>

убедитесь, что вы удалили объявление ContentProvider из списка AndroidManifest.xml в src / main / потому что Gradle не знает, как объединить ContentProviders, имеющие одно и то же имя, но разные полномочия.

наконец, нам может понадобиться доступ к власти в коде. Это можно сделать довольно легко, используя файл BuildConfig и метод buildConfig:

android {   
   // ...

    final PROVIDER_DEBUG = "com.cyrilmottier.android.app.debug.provider"
    final PROVIDER_RELEASE = "com.cyrilmottier.android.app.provider"

   buildTypes {
       debug {
           // ...
           buildConfigField "String", "PROVIDER_AUTHORITY", PROVIDER_DEBUG
       }

       release {
           buildConfigField "String", "PROVIDER_AUTHORITY", PROVIDER_RELEASE
       }
   }
}

благодаря этому обходному пути вы сможете использовать BuildConfig.PROVIDER_AUTHORITY в вашем ProviderContract и установить две разные версии вашего приложения в то же время время.


Оригинал на Google+: https://plus.google.com/u/0/118417777153109946393/posts/EATUmhntaCQ

в то время как пример Кирилла отлично работает, если у вас есть только несколько типов сборки, он быстро усложняется, если у вас есть много типов сборки и/или вкусов продукта, поскольку вам нужно поддерживать много разных AndroidManifest.xml.

наш проект состоит из 3 различных типов сборки и 6 вкусов на общую сумму 18 вариантов сборки, поэтому вместо этого мы добавили поддержку ".res-auto " в полномочиях ContentProvider, которые расширяются до текущего имени пакета и устраняют необходимость поддерживать разные AndroidManifest.xml

/**
 * Version 1.1.
 *
 * Add support for installing multiple variants of the same app which have a
 * content provider. Do this by overriding occurrences of ".res-auto" in
 * android:authorities with the current package name (which should be unique)
 *
 * V1.0 : Initial version
 * V1.1 : Support for ".res-auto" in strings added, 
 *        eg. use "<string name="auth">.res-auto.path.to.provider</string>"
 *
 */
def overrideProviderAuthority(buildVariant) {
    def flavor = buildVariant.productFlavors.get(0).name
    def buildType = buildVariant.buildType.name
    def pathToManifest = "${buildDir}/manifests/${flavor}/${buildType}/AndroidManifest.xml"

    def ns = new groovy.xml.Namespace("http://schemas.android.com/apk/res/android", "android")
    def xml = new XmlParser().parse(pathToManifest)
    def variantPackageName = xml.@package

    // Update all content providers
    xml.application.provider.each { provider ->
        def newAuthorities = provider.attribute(ns.authorities).replaceAll('.res-auto', variantPackageName)
        provider.attributes().put(ns.authorities, newAuthorities)
    }

    // Save modified AndroidManifest back into build dir
    saveXML(pathToManifest, xml)

    // Also make sure that all strings with ".res-auto" are expanded automagically
    def pathToValues = "${buildDir}/res/all/${flavor}/${buildType}/values/values.xml"
    xml = new XmlParser().parse(pathToValues)
    xml.findAll{it.name() == 'string'}.each{item ->
        if (!item.value().isEmpty() && item.value()[0].startsWith(".res-auto")) {
            item.value()[0] = item.value()[0].replace(".res-auto", variantPackageName)
        }
    }
    saveXML(pathToValues, xml)
}

def saveXML(pathToFile, xml) {
    def writer = new FileWriter(pathToFile)
    def printer = new XmlNodePrinter(new PrintWriter(writer))
    printer.preserveWhitespace = true
    printer.print(xml)
}

// Post processing of AndroidManifest.xml for supporting provider authorities
// across build variants.
android.applicationVariants.all { variant ->
    variant.processManifest.doLast {
        overrideProviderAuthority(variant)
    }
}

пример кода можно найти здесь: https://gist.github.com/cmelchior/6988275

поскольку плагин версии 0.8.3 (на самом деле 0.8.1, но он не работал должным образом) вы можете определить ресурсы в файле сборки, так что это может быть более чистым решением, потому что вам не нужно создавать файлы строк или дополнительные папки отладки/выпуска.

построить.gradle

android {
    buildTypes {
        debug{
            resValue "string", "authority", "com.yourpackage.debug.provider"
        }
        release {
            resValue "string", "authority", "com.yourpackage.provider"
        }
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.yourpackage"
   android:versionCode="1"
   android:versionName="1">

   <application>

       <provider
           android:name=".provider.Provider1"
           android:authorities="@string/authority"
           android:exported="false" />

   </application>
</manifest>

Я не знаю, если кто-то упомянул об этом. На самом деле после Android gradle плагин 0.10+, манифест слияния обеспечит официальную поддержку этой функции: http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger

В AndroidManifest.xml, вы можете использовать ${packageName} следующим образом:

<provider
    android:name=".provider.DatabasesProvider"
    android:authorities="${packageName}.databasesprovider"
    android:exported="true"
    android:multiprocess="true" />

и в сборке.gradle вы можете иметь:

productFlavors {
    free {
        packageName "org.pkg1"
    }
    pro {
        packageName "org.pkg2"
    }
}

посмотреть полный пример здесь: https://code.google.com/p/anymemo/source/browse/AndroidManifest.xml#152

и здесь: https://code.google.com/p/anymemo/source/browse/build.gradle#41

использовать ${applicationId} заполнители в xml и BuildConfig.APPLICATION_ID в коде.

вам нужно будет расширить сценарий сборки, чтобы включить заполнители в xml-файлах, отличных от манифеста. Вы можете использовать исходный каталог для каждого варианта сборки, чтобы предоставить различные версии xml-файлов, но обслуживание станет громоздким очень быстро.

AndroidManifest.xml

вы можете использовать заполнитель applicationId из поля в манифесте. Заявлять ваш провайдер, как это:

<provider
    android:name=".provider.DatabaseProvider"
    android:authorities="${applicationId}.DatabaseProvider"
    android:exported="false" />

Примечание ${applicationId} немного. Это заменяется во время сборки с фактическим applicationId для варианта сборки, который строится.

код

ваш ContentProvider должен построить строку полномочий в коде. Он может использовать класс BuildConfig.

public class DatabaseContract {
    /** The authority for the database provider */
    public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".DatabaseProvider";
    // ...
}

Примечание

Я бы предпочел смесь между Кириллом и rciovati. Я думаю, что проще, у вас есть только две модификации.

The build.gradle выглядит так:

android {
    ...
    productFlavors {
        production {
            packageName "package.name.production"
            resValue "string", "authority", "package.name.production.provider"
            buildConfigField "String", "AUTHORITY", "package.name.production.provider"
        }

        testing {
            packageName "package.name.debug"
            resValue "string", "authority", "package.name.debug.provider"
            buildConfigField "String", "AUTHORITY", "package.name.debug.provider"
        }
    }
    ...
}

и AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="package.name" >

    <application
        ...>

        <provider android:name=".contentprovider.Provider" android:authorities="@string/authority" />

    </application>
</manifest>

основываясь на примере @ChristianMelchior, вот мое решение, которое устраняет две проблемы в предыдущих решениях:

  • решения, которые меняют значения.xml в каталоге сборки вызывает полное перестроение ресурсов (включая aapt всех чертежей)

  • по неизвестной причине IntelliJ( и, вероятно, Android Studio) не надежно обрабатывают ресурсы, в результате чего сборка содержит un-replaced .res-auto провайдер власти

это новое решение делает вещи более Gradle путем создания новой задачи и позволяет выполнять инкрементные сборки путем определения входных и выходных файлов.

  1. создать файл (в примере я положил его в variants каталог), отформатированный как XML-файл ресурса, который содержит строковые ресурсы. Они будут объединены в ресурсы приложения, и любое появление .res-auto в значениях будут заменены на вариант имя пакета, например <string name="search_provider">.res-auto.MySearchProvider</string>

  2. добавить С в этом суть к вашему проекту и ссылаться на него из главного build.gradle добавлять apply from: './build_extras.gradle' где-то выше android блок

  3. убедитесь, что вы установили имя пакета по умолчанию, добавив его в android.defaultConfig блок build.gradle

  4. на AndroidManifest.xml и другие файлы конфигурации (например,xml/searchable.xml для поставщиков поиска автоматического завершения), обратитесь к поставщику (например @string/search_provider)

  5. если вам нужно сделать то же имя, вы можете использовать BuildConfig.PACKAGE_NAME переменной, например BuildConfig.PACKAGE_NAME + ".MySearchProvider"

https://gist.github.com/paour/9189462


Обновление: этот метод работает только на Android 2.2.1 и выше. Для более ранних платформ, см. ответ, который имеет свой собственный набор проблем, поскольку в манифесте объединения еще очень грубый по краям…

gradle.строй

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.example.awsomeapp"
        minSdkVersion 9
        targetSdkVersion 23
        versionCode 1
        versionName "1.0.0"
    }

    productFlavors
    {
        prod {
            applicationId = "com.example.awsomeapp"
        }

        demo {
            applicationId = "com.example.awsomeapp.demo"
            versionName = defaultConfig.versionName + ".DEMO"
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            debuggable false
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }

        debug {
            applicationIdSuffix ".debug"
            versionNameSuffix = ".DEBUG"
            debuggable true
        }
    }

    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            // rename the apk
            def file = output.outputFile;
            def newName;
            newName = file.name.replace(".apk", "-" + defaultConfig.versionName + ".apk");
            newName = newName.replace(project.name, "awsomeapp");
            output.outputFile = new File(file.parent, newName);
        }

        //Generate values Content Authority and Account Type used in Sync Adapter, Content Provider, Authenticator
        def valueAccountType = applicationId + '.account'
        def valueContentAuthority = applicationId + '.authority'

        //generate fields in Resource string file generated.xml
        resValue "string", "content_authority", valueContentAuthority
        resValue "string", "account_type", valueAccountType

        //generate fields in BuildConfig class
        buildConfigField "String", "ACCOUNT_TYPE", '"'+valueAccountType+'"'
        buildConfigField "String", "CONTENT_AUTHORITY", '"'+valueContentAuthority+'"'

        //replace field ${valueContentAuthority} in AndroidManifest.xml
        mergedFlavor.manifestPlaceholders = [ valueContentAuthority: valueContentAuthority ]
    }
}

аутентификатора.xml

<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="@string/account_type"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:smallIcon="@drawable/ic_launcher" />

sync_adapter.xml

<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
              android:contentAuthority="@string/content_authority"
              android:accountType="@string/account_type"
              android:userVisible="true"
              android:allowParallelSyncs="false"
              android:isAlwaysSyncable="true"
              android:supportsUploading="true"/>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0.0" package="com.example.awsomeapp">

    <uses-permission android:name="android.permission.GET_ACCOUNTS"/><!-- SyncAdapter and GCM requires a Google account. -->
    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>

    <!-- GCM Creates a custom permission so only this app can receive its messages. -->
    <permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
    <uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>

    <application....
    .......

        <!-- Stub Authenticator --> 
        <service 
                android:name="com.example.awsomeapp.service.authenticator.CAuthenticatorService"
                android:exported="true">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator"/>
            </intent-filter>
            <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator"/>
        </service>
        <!--  -->

        <!-- Sync Adapter -->
        <service
                android:name="com.example.awsomeapp.service.sync.CSyncService"
                android:exported="true"
                android:process=":sync">
            <intent-filter>
                <action android:name="android.content.SyncAdapter"/>
            </intent-filter>
            <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_adapter" />
        </service>
        <!--  -->

        <!-- Content Provider -->
        <provider android:authorities="${valueContentAuthority}"
            android:exported="false" 
            android:name="com.example.awsomeapp.database.contentprovider.CProvider">
        </provider>
        <!--  --> 
    </application>
</manifest>

код:

public static final String CONTENT_AUTHORITY = BuildConfig.CONTENT_AUTHORITY;
public static final String ACCOUNT_TYPE = BuildConfig.ACCOUNT_TYPE;

Я написал блог с образцом проекта Github, который решает эту проблему (и другие подобные проблемы) несколько иначе, чем Кирилл.

http://brad-android.blogspot.com/2013/08/android-gradle-building-unique-build.html

к сожалению, текущая версия (0.4.1) плагина android, похоже, не обеспечивает хорошего решения для этого. У меня еще не было времени попробовать это, но возможным обходным путем для этой проблемы было бы использовать строковый ресурс @string/provider_authority, и использовать это в манифесте: android:authority="@string/provider_authority". Тогда у вас есть res/values/provider.xml в папке res каждого типа сборки, который должен переопределить полномочия, в вашем случае это будет src/debug/res

Я изучил создание xml-файла на лету, но опять же, там, кажется, не будет никаких хороших крючков для него в текущей версии плагина. Я бы рекомендовал ввести запрос функции, хотя, я могу себе представить, что больше людей столкнутся с этой же проблемой.

ответ в этом посте работает для меня.

http://www.kevinrschultz.com/blog/2014/03/23/using-android-content-providers-with-multiple-package-names/

я использую 3 разных вкуса, поэтому я создаю 3 манифеста с контент-провайдером в каждом вкусе, как сказал кевинршульц:

productFlavors {
    free {
        packageName "your.package.name.free"
    }

    paid {
        packageName "your.package.name.paid"
    }

    other {
        packageName "your.package.name.other"
    }
}

ваш основной манифест не включает поставщиков:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- Permissions -->
<application>
    <!-- Nothing about Content Providers at all -->
    <!-- Activities -->
    ...
    <!-- Services -->
    ...
</application>

и ваш манифест в каждом вашем вкусе, включая поставщик.

бесплатно:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
    <!-- Content Providers -->
    <provider
        android:name="your.package.name.Provider"
        android:authorities="your.package.name.free"
        android:exported="false" >
    </provider>
</application>
</manifest>

оплачено:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
    <!-- Content Providers -->
    <provider
        android:name="your.package.name.Provider"
        android:authorities="your.package.name.paid"
        android:exported="false" >
    </provider>
</application>
</manifest>

другое:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
    <!-- Content Providers -->
    <provider
        android:name="your.package.name.Provider"
        android:authorities="your.package.name.other"
        android:exported="false" >
    </provider>
</application>
</manifest>

Почему бы просто не добавить это?

тип.packageNameSuffix =". $type.name"

мое решение заключается в использовании замены заполнителя в AndroidManifest.xml. Он также обрабатывает packageNameSuffix атрибуты, так что вы можете иметь debug и release а также любые другие пользовательские сборки на том же устройстве.

applicationVariants.all { variant ->
    def flavor = variant.productFlavors.get(0)
    def buildType = variant.buildType
    variant.processManifest.doLast {
        println '################# Adding Package Names to Manifest #######################'
        replaceInManifest(variant,
            'PACKAGE_NAME',
            [flavor.packageName, buildType.packageNameSuffix].findAll().join()) // ignores null
    }
}

def replaceInManifest(variant, fromString, toString) {
    def flavor = variant.productFlavors.get(0)
    def buildtype = variant.buildType
    def manifestFile = "$buildDir/manifests/${flavor.name}/${buildtype.name}/AndroidManifest.xml"
    def updatedContent = new File(manifestFile).getText('UTF-8').replaceAll(fromString, toString)
    new File(manifestFile).write(updatedContent, 'UTF-8')
}

у меня есть это на A gist тоже, если вы хотите увидеть, если он развивается позже.

я обнаружил, что это более элегантный подход, чем подходы к анализу нескольких ресурсов и XML.