Gestión de Permisos en Jetpack Compose

      Una de las características más interesantes de cualquier dispositivo moderno es la gestión de permisos, que va a permitir a nuestros usuarios decidir si concede o no un permiso concreto a nuestra aplicación, viéndose modificado por tanto el comportamiento de nuestra app en base a esa decisión.     Para el caso concreto de android, se nos proporcionan multitud de posibilidades, pero google nos proporciona un repositorio de componentes / utilidades que, si bien aún están en fase de desarrollo, nos permite un control bastante bueno de ciertas características, estos componentes forman parte de los componentes Accompanist, de los cuales, muchos acaban formando parte del core de desarrollo, si bien otros, acaban siendo eliminados, ya que no se consideran excesivamente útiles.     En este artículo, vamos a ver un componente específico, y su correspondiente aplicación, que nos va a permitir la gestión  sencilla de los permisos de nuestra app.    Además vamos a encapsular este sistema de

Creación y publicación de librerías Android utilizando MavenCentral (OSSRH)

 

Maven Central System

    Este artículo pretende explicar de la forma más sencilla posible la creación de un proyecto de librería para android enfocado a su despliegue en MavenCentral, a fin de que esté disponible en el buscador de paquetes de Android Studio.

    JetBrains, como no, proporciona también un tutorial bastante cómodo de seguir (practicamente lo mismo que esto, pero con menos imágenes) aunque es para Jetbrains Spaces.

    Para ganar tiempo, y dejar el entorno preparado, lo primero que debemos hacer es registrarnos en Sonatype, siguiendo los siguientes pasos:

  1. Accederemos a la web de incidencias de Sonatype (es un Jira), y nos registraremos como usuario (si no estamos registrados ya).


  2. Pulsaremos sobre Signup, y comenzaremos el proceso de registro.


  3. Una vez acabado el proceso de registro, se nos notifica.


  4. En el primer inicio de sesión, aparece el asistente típico de Jira, en el que configuraremos varios aspectos del sistema.


  5. Una vez tenemos todo configurado (Idioma y avatar), nos aparece la página principal para Jira, en la que tenemos pocas opciones para elegir.


  6. Pulsaremos sobre Crear Incidencia, y seleccionaremos la opción Community Support - Open Source Project Repository Hosting (OSSRH) y como tipo de incidencia, New Project.


  7. Completaremos todos los campos de la incidencia*




    Como podemos ver, tenemos un asunto, que será el "título" del proyecto, una breve descripción de lo que vamos a subir y a continuación los campos realmente importantes:
    1. Group Id: Es la base para los namespaces del proyecto. MUY IMPORTANTE si alojamos nuestro proyecto en GitHub, el package de nuestra librería debe comenzar sí o sí por com.github, en mi caso, establezco com.github.afalabarce
    2. Project URL: Es la url del proyecto, en mi caso en GitHub.
    3. SCM url: Es la url para la descarga y clonado del proyecto (normalmente la misma que la de Project URL, terminada en .git).
    4. Username(s): Será la lista de usuarios (separados por ,) que podrán publicar artifacts en MavenCentral, normalmente, salvo que sea un proyecto colaborativo, seremos nosotros mismos.
    5. Already Synced to Central: Puesto que es la primera vez que solicitamos creación de un nuevo proyecto, indicaremos que NO.
  8. Por último, pulsaremos sobre Crear, a fin de que se genere la incidencia y nos creen nuestro nuevo proyecto de alojamiento de artifacts. Nos aparece la información necesaria de la incidencia. Cuando esté solucionada, esto es, nuestro proyecto creado, se nos notificará por email.


    Nota Importante
    : Ha cambiado el criterio para asignaciones de GroupIds para GitHub, se nos notifica, por lo que debemos modificar el GroupId de nuestra incidencia, y crear un repo en nuestra cuenta de GitHub con el código que nos propone el bot. En cuanto lo realicemos, la incidencia pasa a abierta.


    Nota Importante II: Gracias a la colaboración de @xavijimenezmulet, tenemos también el criterio para creación de proyectos desde BitBucket. Para la creación de proyectos BitBucket deberemos nombrarlos con el prefijo org.bitbucket.

  9. Una vez el Bot (sí, un bot, antes lo hacía un humano y tardaba un par de días) valida todo, se marca la incidencia como resuelta y ya podremos acceder a nexus.


    Como vemos, el bot nos indica dónde debemos acceder para gestionar nuestros artifacts.

    Además se nos proporciona la siguiente url, con información de interés: 


    Hasta este punto, tenemos la primera fase de la creación de un proyecto de librería disponible para su uso a través de MavenCentral.

    La segunda fase consiste en preparar nuestro recien creado proyecto para recibir los aar, POM, etc... Recordemos que MavenCentral está diseñado para Maven (que raro, ¿no?), por lo que tendremos nuestros artifacts, con todo lo que ello implica (POM, Paquetes binarios, Código, Documentación, etc, si procede).

    Para comenzar con esta segunda fase, accederemos a la url que nos propone en bot, en mi caso: https://s01.oss.sonatype.org/#welcome


    Una vez tenemos todo preparado, la siguiente fase de este artículo es la de visibilizar todo el proceso de creación y publicación de nuestra librería de componentes en uno de los sistemas estándar de publicación utilizados por Android Studio (y de cualquier sistema que por ejemplo, utilice gradle, o incluso poms de maven).

    Los siguientes pasos están enfocados en preparar tanto el entorno de nexus, como nuestro propio entorno de desarrollo, ya que hay que realizar ciertas acciones en nuestro equipo:

  1. Hacemos Login, si nos fijamos en la imagen anterior, arriba a la derecha tenemos el enlace Login.



  2. Utilizaremos las mismas credenciales que en la sección anterior.


    Importante: Si intentamos iniciar sesión antes de que se nos haya creado nuestro proyecto (la incidencia no se haya dado por finalizada), se mostrará el siguiente error:

  3. Puesto que ya tenemos acceso a Nexus, por ahora lo dejaremos aparcado... y, citando a @mouredev, comenzaremos a picar...

  4. En nuestro Equipo de trabajo deberemos seguir los siguientes pasos:
    a. 
    Debemos crear una firma gpg, ya que será imprescindible para la firma de nuestros artifacts a la hora de subirlos a MavenCentral, para ello vamos a necesitar las utilidades gpg-tools:






    b. Una vez creadas las claves gpg, las exportaremos a la carpeta de nuestro proyecto, a fin de tenerlas a la mano...
     % mkdir gpgKeys
     % cd gpgKeys 
     % gpg --output public.pgp --armor --export afalabarce@gmail.com
     % gpg --export-secret-keys -o private.kbx
     Los tres siguientes comandos permiten subir nuestra clave a un keyserver (de ubuntu) utilizado por sonatype para validaciones, sin esto, el proceso de publicación fallará. 1234ABCD es el código de clave pública (SHORT, obtenido con el primero de los tres comandos)
     
     % gpg --list-secret-keys --keyid-format SHORT
     % gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 1234ABCD <- --keyserver="" --send-keys="" 1234abcd="" a="" clave.="" clave="" comprobar="" env="" existe.="" gpg="" hkp:="" keyserver.ubuntu.com="" la="" para="" pre="" si="" sirve="">
      
    
    
    Es muy importante (en las capturas dejo la creación "estandar"), crear el fichero de claves privadas con el comando gpg --export-secret-keys -o private.kbxya que de lo contrario, Android Studio nos dará constantemente un error de que no encuentra la cabecera pgp.

    A continuación, explicaremos, brevemente, cómo crear nuestra primera contribución a la comunidad, esto es, una librería de componentes diseñada explícitamente para Android, por lo que el ejemplo requerirá del uso de Android Studio, un ejemplo completo está disponible en mi repo github, con mi propia librería de componentes (y diversas utilidades extra). 

  1. Crearemos un nuevo proyecto para Android Studio, puesto que no vamos a necesitar Activities (o sí, dependerá de lo que vayamos a desarrollar) crearemos el proyecto sin actividades. En mi caso, para este tutorial, voy a crear un proyecto para generar un par de Composables.


  2. Una vez creado el proyecto, podemos apreciar que Android Studio nos ha creado el proyecto con un módulo de tipo app, esto no nos interesa, por lo que realizaremos las siguientes acciones:
    1. Agregar un módulo de tipo Librería.
    2. Eliminar el módulo de tipo App. Al final, el proyecto se nos debe quedar así...


      Como vemos, solo nos queda el proyecto de librería, al que deberemos preparar adecuadamente su build.gradle.kts para las particularidades del proyecto. En mi caso, deberé preparar el build.gradle.kts para que soporte JetpackCompose. En otros casos, se agregarán los implementations necesarios para poder desarrollar la funcionalidad necesaria.

  3. El siguiente paso en la creación de nuestra librería, publicable en MavenCentral, pasa por la adaptación de los ficheros de configuración de nuestro proyecto, para ello:
    1. Modificaremos nuestro fichero local.properties, en el que agregaremos las credenciales necesarias de sonatype para acceso por OSSRH:

      Si nos fijamos
           ## This file is automatically generated by Android Studio.
           # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
           #
           # This file should *NOT* be checked into Version Control Systems,
           # as it contains information specific to your local configuration.
           #
           # Location of the SDK. This is only used by Gradle.
           # For customization when using a Version Control System, please read the
           # header note.
          sdk.dir=/Users/afalabarce/Library/Android/sdk
          signing.keyId = 1234ABCD # Clave en hexadecimal de nuestra clave privada GPG, se obtiene con gpg --list-secret-keys --keyid-format SHORT
          signing.password = miPasswordGPG
          signing.secretKeyRingFile = /Users/afalabarce/Desarrollo/AndroidStudioProjects/JetpackComposeComponents/gpgKeys/private.kbx
          ossrhUsername = miUsuarioSonatype
          ossrhPassword = miPasswordSonatype
    2. Modificaremos nuestro build.gradle.kts (el del módulo de librería), que, gracias al nuevo sistema de plugins se simplifica un poco (para ver cómo crear un módulo de este tipo con los build.gradle antiguos, puedes verlo en mi repo original). Este build.gradle.kts quedaría como sigue:

      import java.io.FileInputStream
      import java.util.Properties
      
      plugins {
          id("com.android.library")
          id("org.jetbrains.kotlin.android")
          id ("maven-publish") // Support for maven publishing artifacts
          id ("signing") // Support for signing artifacts
      }
      
      android {
          namespace = "io.github.afalabarce.jetpackcompose.ads.admob"
          compileSdk = 33
      
          defaultConfig {
              minSdk = 23
      
              testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
              consumerProguardFiles("consumer-rules.pro")
          }
      
          buildTypes {
              release {
                  isMinifyEnabled = false
                  proguardFiles(
                      getDefaultProguardFile("proguard-android-optimize.txt"),
                      "proguard-rules.pro"
                  )
              }
          }
          compileOptions {
              sourceCompatibility = JavaVersion.VERSION_11
              targetCompatibility = JavaVersion.VERSION_11
          }
          kotlinOptions {
              jvmTarget = "11"
          }
      
          buildFeatures {
              compose = true
          }
          composeOptions {
              kotlinCompilerExtensionVersion  = Constants.composeCompilerVersion
          }
      }
      
      dependencies {
      
          implementation("androidx.core:core-ktx:${Constants.androidxCoreKtxVersion}")
          implementation("androidx.appcompat:appcompat:${Constants.androidxAppCompatVersion}")
          implementation ("androidx.lifecycle:lifecycle-runtime-ktx:${Constants.lifeCycleRuntimeVersion}")
          implementation ("androidx.lifecycle:lifecycle-runtime-compose:${Constants.lifeCycleRuntimeVersion}")
          implementation ("androidx.compose.ui:ui:${Constants.composeVersion}")
          implementation ("androidx.compose.ui:ui-tooling:${Constants.composeVersion}")
          implementation ("androidx.compose.ui:ui-tooling-preview:${Constants.composeVersion}")
          implementation ("androidx.activity:activity-compose:${Constants.activityComposeVersion}")
          implementation ("androidx.constraintlayout:constraintlayout-compose:${Constants.constraintLayoutComposeVersion}")
          implementation ("androidx.compose.material3:material3:${Constants.composeMaterial3Version}")
      
          // ads dependencies
          implementation (platform("com.google.firebase:firebase-bom:${Constants.firebaseBomVersion}"))
          implementation ("com.google.firebase:firebase-appcheck-playintegrity")
          implementation ("com.google.firebase:firebase-appcheck-debug:${Constants.firebaseAppCheckDebugVersion}")
          implementation ("com.google.firebase:firebase-messaging:${Constants.firebaseMessagingVersion}")
          implementation ("com.google.android.gms:play-services-ads:${Constants.gmsPlayServicesAdsVersion}")
          implementation ("com.google.android.gms:play-services-ads-identifier:${Constants.gmsPlayServicesAdsVersionIdentifierVersion}")
          testImplementation("junit:junit:4.13.2")
          androidTestImplementation("androidx.test.ext:junit:1.1.5")
          androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
      }
      
      tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
          kotlinOptions {
              freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
          }
      }
      
      val androidSourcesJarTask by tasks.register("androidSourcesJar", Jar::class.java) {
          archiveClassifier.set("sources")
      
          android.sourceSets.toTypedArray().forEach {
              println("${it.name}(${it.java.name}) -> ${it.java}")
          }
          val mainSourceSet = android.sourceSets.first{sourceSets -> sourceSets.name == "main"}
      
          from(mainSourceSet.java.srcDirs).source
      
      }
      
      artifacts {
          archives(androidSourcesJarTask)
      }
      
      group = SonatypePublishing.publishedGroupId
      version = SonatypePublishing.libraryVersionId
      
      val signingData = mutableMapOf(
          "signing.keyId" to "",
          "signing.password" to "",
          "signing.secretKeyRingFile" to "",
          "ossrhUsername" to "",
          "ossrhPassword" to ""
      )
      
      val secretPropsFile = project.rootProject.file("local.properties")
      if (secretPropsFile.exists()) {
          println("Found secret props file, loading props")
          val props = Properties()
          props.load(FileInputStream(secretPropsFile))
          for ((name, value) in props) {
              println("Prop: $name -> $value")
              signingData[name.toString()] = value.toString()
          }
      }
      
      publishing {
          publications {
              create<MavenPublication>("Maven"){
                  groupId = SonatypePublishing.publishedGroupId
                  artifactId = SonatypePublishing.artifact
                  version = SonatypePublishing.libraryVersionCode
      
                  println ("groupId: ${SonatypePublishing.publishedGroupId}")
                  println ("Artifact: ${SonatypePublishing.artifact}")
                  println ("Version: ${SonatypePublishing.libraryVersionCode}")
              }
              withType<MavenPublication>{
                  println(project.name)
                  // Two artifacts, the `aar` and the sources
                  artifact("$buildDir/outputs/aar/${project.name}-release.aar")
                  artifact(androidSourcesJarTask)
      
      
                  pom {
                      packaging = "aar"
                      name.set(SonatypePublishing.artifact)
                      description.set(SonatypePublishing.libraryDescription)
                      url.set(SonatypePublishing.gitUrl)
                      licenses {
                          license {
                              name.set(SonatypePublishing.licenseName)
                              url.set(SonatypePublishing.licenseUrl)
                          }
                      }
                      issueManagement {
                          system.set("Github")
                          url.set("https://github.com/afalabarce/jetpackcompose-admob/issues")
                      }
                      scm {
                          connection.set("scm:git:github.com/afalabarce/jetpackcompose-admob.git")
                          developerConnection.set("scm:git:ssh://github.com/afalabarce/jetpackcompose-admob.git")
                          url.set("https://github.com/afalabarce/jetpackcompose-admob/tree/master")
                      }
                      developers {
                          developer {
                              id.set(SonatypePublishing.developerId)
                              name.set(SonatypePublishing.developerName)
                              email.set(SonatypePublishing.developerEmail)
                          }
                      }
      
                      // A slightly hacky fix so that your POM will include any transitive dependencies
                      // that your library builds upon
                      withXml {
                          val dependenciesNode = asNode().appendNode("dependencies")
                          project.configurations.implementation.configure {
                              dependencies.forEach { dependency ->
                                  val dependencyNode = dependenciesNode.appendNode("dependency")
                                  dependencyNode.appendNode("groupId", dependency.group)
                                  dependencyNode.appendNode("artifactId", dependency.name)
                                  dependencyNode.appendNode("version", dependency.version)
                              }
                          }
      
                      }
                  }
              }
          }
          repositories {
              // The repository to publish to, Sonatype/MavenCentral
              maven {
                  // This is an arbitrary name, you may also use "mavencentral" or
                  // any other name that's descriptive for you
                  name = "sonatype"
                  // these urls depend on the configuration provided to the user by sonatype
                  val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
                  val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
                  // You only need this if you want to publish snapshots, otherwise just set the URL
                  // to the release repo directly
      
                  setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
                  
                  // The username and password we've fetched earlier
                  credentials {
                      username = signingData["ossrhUsername"]
                      password = signingData["ossrhPassword"]
                  }
              }
          }
      }
      
      
      signing {
          val keyRingPrivateKey = File(signingData["signing.secretKeyRingFile"].toString()).readText(Charsets.UTF_8)
          println("Signing KeyId: ${signingData["signing.keyId"]}")
          println("Signing Password: ${signingData["signing.password"]}")
          println("Signing Key:\n$keyRingPrivateKey\n---------")
          useInMemoryPgpKeys(
              signingData["signing.keyId"],
              keyRingPrivateKey,
              signingData["signing.password"],
          )
          publishing.publications.forEach { publication ->
              println("Signing [${publication.name}] Publication...")
              sign(publication)
          }
      
      }

      Si nos fijamos un poco en este build.gradle.kts, tenemos ciertas secciones que son poco comunes (o nada comunes), estas son signing, repositories y publishing, las cuales se alimentan de las configuraciones que hayamos establecido en nuestro local.properties, a fin de configurar el despliegue, creando los pom necesarios, que serán subidos al repo de mavenCentral.

    3. En este punto, ya tenemos todo configurado, quedando únicamente, y ya desde el IDE (después de sincronizar el proyecto con gradle), publicar en mavenCentral.

    4.  Por último, y una vez gradle nos da el ok a la subida, podremos comprobar en nexus que todo ha ido bien:

    5. En este punto, ya tenemos nuestro aar subido a MavenCentral, pero aún no ha sido publicado.

    6. Guardaremos el artifact, seleccionándolo y pulsando sobre Close. Nos pedirá una descripción que, si bien es opcional, es muy recomendable, para proporcionar, al menos, algo de información sobre la release que acabamos de publicar.


      Una vez confirmado el cierre, durante unos segundos pasa a "Operation in progress" y a continuación a "Closed".


  4. Una vez cerrado, ya solo nos queda publicar, pulsando sobre Release. 




    El resultado final, nuestro artifact publicado en el repositorio release de mavenCentral, cuestión de tiempo que aparezca en las búsquedas de librerías de Android Studio (unas tres horas). Si tenemos prisa, podemos configurar directamente en build.gradle.kts nuestro recien subido artifact, ya que tras unos quince minutos está disponible.

    Aunque, como digo, lo más satisfactorio es que, tras esas 3 horas, si en android studio buscamos nuestra librería...
    Nuestro aporte a la comunidad está disponible para uso y disfrute de todos 😎




Comentarios

Entradas populares de este blog

Implementación de DecimalFormatSymbol en KMP

Gestión de Permisos en Jetpack Compose