package com.hivemq.extensions.helloworld

import com.hivemq.extension.sdk.api.annotations.NotNull
import com.hivemq.extension.sdk.api.auth.SimpleAuthenticator
import com.hivemq.extension.sdk.api.auth.parameter.SimpleAuthInput
import com.hivemq.extension.sdk.api.auth.parameter.SimpleAuthOutput
import com.hivemq.extensions.helloworld.models.Device
import com.hivemq.extensions.helloworld.network.MSGraphClient
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
import java.security.GeneralSecurityException
import java.security.cert.CertificateFactory
import java.security.cert.CertificateParsingException
import java.security.cert.X509Certificate
import java.util.*
import javax.naming.ldap.LdapName
import kotlin.jvm.optionals.getOrElse

class AADAuthenticator(
    private val graphClient: MSGraphClient
) : SimpleAuthenticator {
    private val log: Logger = LoggerFactory.getLogger(
        AADAuthenticator::class.java
    )

    @OptIn(ExperimentalStdlibApi::class)
    override fun onConnect(simpleAuthInput: SimpleAuthInput, simpleAuthOutput: SimpleAuthOutput) {
        val packet = simpleAuthInput.connectionInformation;
        val tlsInfo = packet.clientTlsInformation.getOrElse { return simpleAuthOutput.failAuthentication() }
        val certificate = tlsInfo.clientCertificate.getOrElse { return simpleAuthOutput.failAuthentication() }
        try {
            val device = getDeviceInfo(simpleAuthInput.connectPacket.clientId)
            device.deviceTemplate.firstOrNull()?.let { template ->
                this.graphClient.getMtlsConfig(template.mutualTLSOauthConfigurationID)
            }?.let { tlsAuth ->
                try {
                    tlsAuth.certificateAuthorities.map { authority ->
                        authority.certificate?.let { cert ->
                            val decoded: ByteArray = Base64.getDecoder()
                                .decode(cert)
                            certificate.verify(
                                CertificateFactory.getInstance("X.509")
                                    .generateCertificate(
                                        ByteArrayInputStream(decoded)
                                    )
                                    .publicKey
                            )

                            val urisInSAN = this.extractSANList(certificate)
                            val externalDeviceIds = urisInSAN.mapNotNull {
                                if (it.startsWith("urn:uuid:")) {
                                    it.substring(9, it.length)
                                } else {
                                    null
                                }
                            }
                            if (externalDeviceIds.any {
                                    it.equals(simpleAuthInput.connectPacket.userName.get(), true)
                                }) {
                                return simpleAuthOutput.authenticateSuccessfully()
                            }
                        }
                    }

                } catch (exception: GeneralSecurityException) {
                    log.info("cert not valid, trying next..")
                }
            }
        } catch (exception: Exception) {
            log.info("Authentication Failed: $exception")
        }
        simpleAuthOutput.failAuthentication()
    }

    private fun getDeviceInfo(deviceId: String): Device {
        return this.graphClient.getDeviceById(deviceId)
    }

    private fun extractSANList(clientCert: X509Certificate): ArrayList<String> {

        try {
            val subAltNames = clientCert.subjectAlternativeNames
            val hostNameList = ArrayList<String>()

            if (subAltNames != null) {
                for (altName in subAltNames) {
                    if (altName.size < 2) continue
                    when (altName[0] as Int?) {
                        6 -> {
                            val data = altName[1]!!
                            if (data is String) {
                                hostNameList.add(data)
                            }
                        }
                        else -> {}
                    }
                }
            }
            log.info("Parsed hostNames: " + hostNameList.joinToString("; "))
            log.info(clientCert.subjectX500Principal.name)
            return hostNameList
        } catch (e: CertificateParsingException) {
            throw RuntimeException(e)
        }
    }

    private fun extractSubjectDN(clientCert: X509Certificate): String {
        val ldap = LdapName(clientCert.subjectX500Principal.name)
        val cn = ldap.rdns.first {
            it.type.lowercase() == "cn"
        }

        log.info("Found CN ${cn.value}")
        return cn.value.toString()
    }

    companion object {
        private val log: @NotNull Logger? = LoggerFactory.getLogger(
            AADAuthenticator::class.java
        )
    }
}


/*
 * Copyright 2018-present HiveMQ GmbH
 *
 * 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.hivemq.extensions.helloworld

import com.hivemq.extension.sdk.api.annotations.NotNull
import com.hivemq.extension.sdk.api.interceptor.publish.PublishInboundInterceptor
import com.hivemq.extension.sdk.api.interceptor.publish.parameter.PublishInboundInput
import com.hivemq.extension.sdk.api.interceptor.publish.parameter.PublishInboundOutput
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets

/**
 * This is a very simple [PublishInboundInterceptor],
 * it changes the payload of every incoming PUBLISH with the topic 'hello/world' to 'Hello World!'.
 *
 * @author Yannick Weber
 * @since 4.3.1
 */
class HelloWorldInterceptor : PublishInboundInterceptor {
    override fun onInboundPublish(
        publishInboundInput: @NotNull PublishInboundInput?,
        publishInboundOutput: @NotNull PublishInboundOutput?
    ) {
        val publishPacket = publishInboundOutput!!.publishPacket
        if ("hello/world" == publishPacket.topic) {
            val payload = ByteBuffer.wrap("Wrong World!".toByteArray(StandardCharsets.UTF_8))
            publishPacket.setPayload(payload)
        }
    }
}


/*
 * Copyright 2018-present HiveMQ GmbH
 *
 * 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.hivemq.extensions.helloworld

import com.hivemq.extension.sdk.api.annotations.NotNull
import com.hivemq.extension.sdk.api.events.client.ClientLifecycleEventListener
import com.hivemq.extension.sdk.api.events.client.parameters.AuthenticationSuccessfulInput
import com.hivemq.extension.sdk.api.events.client.parameters.ConnectionStartInput
import com.hivemq.extension.sdk.api.events.client.parameters.DisconnectEventInput
import com.hivemq.extension.sdk.api.packets.general.MqttVersion
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * This is a very simple [ClientLifecycleEventListener]
 * which logs the MQTT version and identifier of every connecting client.
 *
 * @author Florian LimpÃ¶ck
 * @since 4.0.0
 */
class HelloWorldListener : ClientLifecycleEventListener {
    override fun onMqttConnectionStart(connectionStartInput: @NotNull ConnectionStartInput?) {
        val version = connectionStartInput!!.connectPacket.mqttVersion
        when (version) {
            MqttVersion.V_5 -> log!!.info(
                "MQTT 5 client connected with id: {} ",
                connectionStartInput.clientInformation.clientId
            )

            MqttVersion.V_3_1_1 -> log!!.info(
                "MQTT 3.1.1 client connected with id: {} ",
                connectionStartInput.clientInformation.clientId
            )

            MqttVersion.V_3_1 -> log!!.info(
                "MQTT 3.1 client connected with id: {} ",
                connectionStartInput.clientInformation.clientId
            )
        }
    }

    override fun onAuthenticationSuccessful(authenticationSuccessfulInput: @NotNull AuthenticationSuccessfulInput?) {}
    override fun onDisconnect(disconnectEventInput: @NotNull DisconnectEventInput?) {
        log!!.info("Client disconnected with id: {} ", disconnectEventInput!!.clientInformation.clientId)
    }

    companion object {
        private val log: @NotNull Logger? = LoggerFactory.getLogger(
            HelloWorldListener::class.java
        )
    }
}


/*
 * Copyright 2018-present HiveMQ GmbH
 *
 * 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.hivemq.extensions.helloworld

import com.hivemq.extension.sdk.api.ExtensionMain
import com.hivemq.extension.sdk.api.annotations.NotNull
import com.hivemq.extension.sdk.api.client.ClientContext
import com.hivemq.extension.sdk.api.client.parameter.InitializerInput
import com.hivemq.extension.sdk.api.events.client.parameters.ClientLifecycleEventListenerProviderInput
import com.hivemq.extension.sdk.api.parameter.ExtensionStartInput
import com.hivemq.extension.sdk.api.parameter.ExtensionStartOutput
import com.hivemq.extension.sdk.api.parameter.ExtensionStopInput
import com.hivemq.extension.sdk.api.parameter.ExtensionStopOutput
import com.hivemq.extension.sdk.api.services.Services
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * This is the main class of the extension,
 * which is instantiated either during the HiveMQ start up process (if extension is enabled)
 * or when HiveMQ is already started by enabling the extension.
 *
 * @author Florian LimpÃ¶ck
 * @since 4.0.0
 */
class HelloWorldMain : ExtensionMain {
    override fun extensionStart(
        extensionStartInput: @NotNull ExtensionStartInput?,
        extensionStartOutput: @NotNull ExtensionStartOutput?
    ) {
        try {
//            addClientLifecycleEventListener();
//            addPublishModifier();
            val extensionInformation = extensionStartInput!!.extensionInformation
            log!!.info("Started " + extensionInformation.name + ":" + extensionInformation.version)
            Services.securityRegistry().setAuthenticatorProvider(AADAuthenticationProvider())
        } catch (e: Exception) {
            log!!.error("Exception thrown at extension start: ", e)
        }
    }

    override fun extensionStop(
        extensionStopInput: @NotNull ExtensionStopInput?,
        extensionStopOutput: @NotNull ExtensionStopOutput?
    ) {
        val extensionInformation = extensionStopInput!!.extensionInformation
        log!!.info("Stopped " + extensionInformation.name + ":" + extensionInformation.version)
    }

    private fun addClientLifecycleEventListener() {
        val eventRegistry = Services.eventRegistry()
        val helloWorldListener = HelloWorldListener()
        eventRegistry.setClientLifecycleEventListener { input: ClientLifecycleEventListenerProviderInput? -> helloWorldListener }
    }

    private fun addPublishModifier() {
        val initializerRegistry = Services.initializerRegistry()
        val helloWorldInterceptor = HelloWorldInterceptor()
        initializerRegistry.setClientInitializer { initializerInput: InitializerInput?, clientContext: ClientContext ->
            clientContext.addPublishInboundInterceptor(
                helloWorldInterceptor
            )
        }
    }

    companion object {
        private val log: @NotNull Logger? = LoggerFactory.getLogger(
            HelloWorldMain::class.java
        )
    }
}


package com.hivemq.extensions.helloworld.models

data class AzureAuthContext (
    val tenantId: String,
    val clientId: String,
    val clientSecret: String,
    val scope: String,
    val grantType: String,
    val hostname: String
)


package com.hivemq.extensions.helloworld.models

class AzureAuthRequest {
}


package com.hivemq.extensions.helloworld.models

import com.google.gson.annotations.SerializedName

data class AzureAuthResponse(
    @SerializedName("token_type")
    val tokenType: String,
    @SerializedName("access_token")
    val accessToken: String
)


package com.hivemq.extensions.helloworld.models

import com.google.gson.annotations.SerializedName

data class Device  (
    @SerializedName("@odata.context")
    val odataContext: String,

    val id: String,
    val deletedDateTime: Any? = null,
    val accountEnabled: Boolean,
    val approximateLastSignInDateTime: String,
    val complianceExpirationDateTime: Any? = null,
    val createdDateTime: String,
    val deviceCategory: Any? = null,

    @SerializedName("deviceId")
    val deviceID: String,

    val deviceMetadata: Any? = null,
    val deviceOwnership: Any? = null,
    val deviceVersion: Long,
    val displayName: Any? = null,
    val domainName: Any? = null,
    val enrollmentProfileName: Any? = null,
    val enrollmentType: Any? = null,
    val externalSourceName: String,
    val isCompliant: Any? = null,
    val isManaged: Any? = null,
    val isManagementRestricted: Any? = null,
    val isRooted: Any? = null,
    val managementType: Any? = null,
    val manufacturer: String,

    @SerializedName("mdmAppId")
    val mdmAppID: Any? = null,

    val model: String,
    val onPremisesLastSyncDateTime: Any? = null,
    val onPremisesSyncEnabled: Any? = null,
    val operatingSystem: String,
    val operatingSystemVersion: String,
    val hostnames: List<Any?>,

    @SerializedName("physicalIds")
    val physicalIDS: List<Any?>,

    val profileType: String,
    val registrationDateTime: Any? = null,
    val sourceType: String,
    val systemLabels: List<Any?>,
    val trustType: Any? = null,

    @SerializedName("alternativeSecurityIds")
    val alternativeSecurityIDS: List<AlternativeSecurityID>,

    val extensionAttributes: ExtensionAttributes,
    val deviceTemplate: List<DeviceTemplate>
)

data class AlternativeSecurityID (
    val type: Long,
    val identityProvider: Any? = null,
    val key: String
)

data class DeviceTemplate (
    val id: String,
    val deletedDateTime: Any? = null,

    @SerializedName("mutualTlsOauthConfigurationId")
    val mutualTLSOauthConfigurationID: String,

    @SerializedName("mutualTlsOauthConfigurationTenantId")
    val mutualTLSOauthConfigurationTenantID: String,

    val deviceAuthority: String,
    val manufacturer: String,
    val model: String,
    val operatingSystem: String
)

data class ExtensionAttributes (
    val extensionAttribute1: Any? = null,
    val extensionAttribute2: Any? = null,
    val extensionAttribute3: Any? = null,
    val extensionAttribute4: Any? = null,
    val extensionAttribute5: Any? = null,
    val extensionAttribute6: Any? = null,
    val extensionAttribute7: Any? = null,
    val extensionAttribute8: Any? = null,
    val extensionAttribute9: Any? = null,
    val extensionAttribute10: Any? = null,
    val extensionAttribute11: Any? = null,
    val extensionAttribute12: Any? = null,
    val extensionAttribute13: Any? = null,
    val extensionAttribute14: Any? = null,
    val extensionAttribute15: Any? = null
)


package com.hivemq.extensions.helloworld.models

import com.google.gson.annotations.SerializedName

data class MtlsAuthConfiguration(

    @SerializedName("id")
    var id: String,

    @SerializedName("deletedDateTime")
    var deletedDateTime: String? = null,

    @SerializedName("displayName")
    var displayName: String,

    @SerializedName("tlsClientAuthParameter")
    var tlsClientAuthParameter: TlsClientAuthParamter,

    @SerializedName("certificateAuthorities")
    var certificateAuthorities: ArrayList<CertificateAuthority> = arrayListOf()
)

enum class TlsClientAuthParamter {
    @SerializedName("tls_client_auth_subject_dn")
    SubjectDistinguishedName,

    @SerializedName("tls_client_auth_san_dns")
    SubjectAlternativeNameDNS,

    @SerializedName("tls_client_auth_san_uri")
    SubjectAlternativeNameUri,

    @SerializedName("tls_client_auth_san_ip")
    SubjectAlternativeNameIp,

    @SerializedName("tls_client_auth_san_email")
    SubjectAlternativeNameEmail,

    @SerializedName("unknownFutureValue")
    Unknown
}

data class CertificateAuthority(

    @SerializedName("isRootAuthority")
    var isRootAuthority: Boolean? = null,

    @SerializedName("certificateRevocationListUrl")
    var certificateRevocationListUrl: String? = null,

    @SerializedName("deltaCertificateRevocationListUrl")
    var deltaCertificateRevocationListUrl: String? = null,

    @SerializedName("certificate")
    var certificate: String? = null,

    @SerializedName("issuer")
    var issuer: String? = null,

    @SerializedName("issuerSki")
    var issuerSki: String? = null

)


package com.hivemq.extensions.helloworld.network

import com.hivemq.extensions.helloworld.models.AzureAuthContext
import okhttp3.Interceptor
import okhttp3.Response
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class AuthorizationHeaderInterceptor private constructor(): Interceptor {
    private val azureAuthenticator: AzureAuthenticator by lazy {
        AzureAuthenticator(
            AzureAuthContext(
                hostname = "https://login.windows-ppe.net/",
                tenantId = "bf66f47a-6971-4c90-a7a9-99be82dea167",
                clientId = "6ea23442-5896-44ff-b773-49405922bfc8",
                clientSecret = System.getenv("GRAPH_CLIENT_SECRET"),
                scope = "https://graph.microsoft-ppe.com/.default",
                grantType = "client_credentials"
            )
        )
    }
    private val log: Logger = LoggerFactory.getLogger(
        AuthorizationHeaderInterceptor::class.java
    )

    private var authToken: String? = null

    override fun intercept(chain: Interceptor.Chain): Response {

        synchronized(this) {
            if (this.authToken == null) {
                this.authToken = this.azureAuthenticator.authenticate().accessToken;
            }
        }

        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${this.authToken}")
            .build()
        val response = chain.proceed(request)
        log.info("ResponseCode: " + response.code())
        if (response.code() == 401) {
            synchronized(this) {
                if (this.authToken == null) {
                    this.authToken = this.azureAuthenticator.authenticate().accessToken;
                }
            }
            val request = chain.request().newBuilder()
                .addHeader("Authorization", "Bearer ${this.authToken}")
                .build()
            log.info("Retry ResponseCode: " + response.code())
            return chain.proceed(request)
        }
        return response
    }
    companion object {
        val INSTANCE: Interceptor by lazy() { AuthorizationHeaderInterceptor() }
    }

}


package com.hivemq.extensions.helloworld.network

import com.hivemq.extensions.helloworld.models.AzureAuthContext
import com.hivemq.extensions.helloworld.models.AzureAuthResponse
import com.hivemq.extensions.helloworld.services.AzureAuthService
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.security.sasl.AuthenticationException

class AzureAuthenticator (
    private val context: AzureAuthContext
){
    private val service: AzureAuthService
    init {
        val retrofit = Retrofit.Builder().baseUrl(context.hostname)
            .addConverterFactory(GsonConverterFactory.create())
            .client(HttpClient.WIthResponseInterceptor)
            .build()

        this.service = retrofit.create(AzureAuthService::class.java)
    }

    internal fun authenticate(): AzureAuthResponse {
        var response = this.service.authenticate(
            tenantId = this.context.tenantId,
            clientId = this.context.clientId,
            clientSecret = this.context.clientSecret,
            scope = this.context.scope,
            grantType = this.context.grantType
        )
        var res = response.execute();
        System.out.println(res.errorBody()?.string())
        return res.body() ?: throw AuthenticationException("Authorization with AAD failed")
    }

}


package com.hivemq.extensions.helloworld.network

import okhttp3.OkHttpClient

class HttpClient {
    companion object {
        val WithAuthorizationHeader: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            OkHttpClient.Builder()
                .addNetworkInterceptor(AuthorizationHeaderInterceptor.INSTANCE)
                .build();
        }

        val WIthResponseInterceptor: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            OkHttpClient.Builder()
                .addNetworkInterceptor(ResponseInterceptor.INSTANCE)
                .build();
        }
    }
}


package com.hivemq.extensions.helloworld.network

import com.hivemq.extensions.helloworld.models.Device
import com.hivemq.extensions.helloworld.models.MtlsAuthConfiguration
import com.hivemq.extensions.helloworld.services.GraphDeviceService
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MSGraphClient {
    private val service: GraphDeviceService

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl("http://graph.microsoft-ppe.com/beta/")
            .addConverterFactory(GsonConverterFactory.create())
            .client(HttpClient.WithAuthorizationHeader)
            .build()

        this.service = retrofit.create(GraphDeviceService::class.java)
    }

    fun getDeviceById(deviceId: String): Device {
        val response = this.service.getDeviceById(deviceId).execute();
        return response.body() ?: throw RuntimeException("No such device found")
    }

    fun getMtlsConfig(mutualTLSOauthConfigurationID: String): MtlsAuthConfiguration {
        val response = this.service.getMtlsConfigById(mutualTLSOauthConfigurationID).execute()
        return response.body() ?: throw RuntimeException("No such mtlsconfig found")
    }

}


package com.hivemq.extensions.helloworld.network

import com.hivemq.extensions.helloworld.models.AzureAuthContext
import okhttp3.Interceptor
import okhttp3.Response
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class ResponseInterceptor private constructor(): Interceptor {

    private val log: Logger = LoggerFactory.getLogger(
        ResponseInterceptor::class.java
    )

    private var authToken: String? = null

    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())
        log.info("ResponseCode: " + response.code())

        return response
    }
    companion object {
        val INSTANCE: Interceptor by lazy() { ResponseInterceptor() }
    }

}


package com.hivemq.extensions.helloworld.services

import com.hivemq.extensions.helloworld.models.AzureAuthResponse
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import retrofit2.http.Path

interface AzureAuthService {
    @POST("/{tenantId}/oauth2/v2.0/token")
    @FormUrlEncoded
    fun authenticate(
        @Path("tenantId") tenantId: String,
        @Field("client_id") clientId: String,
        @Field("client_secret") clientSecret: String,
        @Field("scope") scope: String,
        @Field("grant_type") grantType: String,
    ): Call<AzureAuthResponse>
}


package com.hivemq.extensions.helloworld.services

import com.hivemq.extensions.helloworld.models.Device
import com.hivemq.extensions.helloworld.models.MtlsAuthConfiguration
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path

interface GraphDeviceService {
    @GET("devices/{deviceId}?\$expand=deviceTemplate")
    fun getDeviceById(@Path("deviceId") deviceId: String): Call<Device>

    @GET("devices/?\$filter=alternativeSecurityIds/any(a:a/type eq 2 and a/key eq '{externalDeviceId}'")
    fun getDeviceByExternalId(@Path("externalDeviceId") deviceId: String): Call<List<Device>>
    @GET("directory/certificateAuthorities/mutualTlsOauthConfigurations/{tlsConfigId}")
    fun getMtlsConfigById(@Path("tlsConfigId") mutualTLSOauthConfigurationID: String): Call<MtlsAuthConfiguration>
}


package frauddetectionservice

import org.apache.kafka.clients.consumer.ConsumerConfig.*
import org.apache.kafka.clients.consumer.KafkaConsumer
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.kafka.common.serialization.ByteArrayDeserializer
import oteldemo.Demo.*
import java.time.Duration.ofMillis
import java.util.Properties
import kotlin.system.exitProcess

const val topic = "orders"
const val groupID = "frauddetectionservice"

fun main(args: Array<String>) {
    val props = Properties()
    props[KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java.name
    props[VALUE_DESERIALIZER_CLASS_CONFIG] = ByteArrayDeserializer::class.java.name
    props[GROUP_ID_CONFIG] = groupID
    val bootstrapServers = System.getenv("KAFKA_SERVICE_ADDR")
    if (bootstrapServers == null) {
        println("KAFKA_SERVICE_ADDR is not supplied")
        exitProcess(1)
    }
    props[BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers
    val consumer = KafkaConsumer<String, ByteArray>(props).apply {
        subscribe(listOf(topic))
    }

    var totalCount = 0L

    consumer.use {
        while (true) {
            totalCount = consumer
                .poll(ofMillis(100))
                .fold(totalCount) { accumulator, record ->
                    val newCount = accumulator + 1
                    val orders = OrderResult.parseFrom(record.value())
                    println("Consumed record with orderId: ${orders.orderId}, and updated total count to: $newCount")
                    newCount
                }
        }
    }
}


package com.example.notification_hubs_sample_app_kotlin

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.notification_hubs_sample_app_kotlin", appContext.packageName)
    }
}


package com.example.notification_hubs_sample_app_kotlin

import android.os.Bundle

import androidx.appcompat.app.AppCompatActivity
import androidx.viewpager.widget.ViewPager

import com.example.notification_hubs_sample_app_kotlin.ui.main.SectionsPagerAdapter
import com.google.android.material.tabs.TabLayout
import com.microsoft.windowsazure.messaging.notificationhubs.InstallationTemplate
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationHub

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager)
        val viewPager: ViewPager = findViewById(R.id.view_pager)
        viewPager.adapter = sectionsPagerAdapter
        val tabs: TabLayout = findViewById(R.id.tabs)
        tabs.setupWithViewPager(viewPager)

        NotificationHub.start(this.application, BuildConfig.hubName, BuildConfig.hubListenConnectionString)
        NotificationHub.addTag("userAgent:com.example.notification_hubs_sample_app_kotlin:0.1.0")
        val testTemplate = InstallationTemplate()
        testTemplate.body = "{\"data\":{\"message\":\"Notification Hub test notification: \$myTextProp\"}}"
        NotificationHub.setTemplate("testTemplate", testTemplate)
    }
}


package com.example.notification_hubs_sample_app_kotlin

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class NotificationDetailActivity : AppCompatActivity() {
    companion object {
        private const val INTENT_KEY_PREFIX = "com.example.notification_hubs_sample_app_kotlin.detail:"
        const val INTENT_TITLE_KEY = INTENT_KEY_PREFIX + "title"
        const val INTENT_BODY_KEY = INTENT_KEY_PREFIX + "body"

        private var RESERVED_KEYS: HashSet<String> = HashSet()

        private fun getDataKeys(i: Intent): Iterable<String> {
            val retval: ArrayList<String> = ArrayList()
            for (key in i.extras!!.keySet()) {
                if (!RESERVED_KEYS.contains(key)) {
                    retval.add(key)
                }
            }
            retval.sort()
            return retval
        }

        init {
            RESERVED_KEYS.add(INTENT_TITLE_KEY)
            RESERVED_KEYS.add(INTENT_BODY_KEY)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val info: Intent = intent
        setContentView(R.layout.activity_notification_detail)
        val titleValue: TextView = findViewById(R.id.detailTitleValue)
        titleValue.text = info.getStringExtra(INTENT_TITLE_KEY)
        val bodyValue: TextView = findViewById(R.id.detail_body_view)
        bodyValue.text = info.getStringExtra(INTENT_BODY_KEY)
        val detailContent: LinearLayout = findViewById(R.id.detail_content)
        for (key in getDataKeys(info)) {
            val dataRow: View = layoutInflater.inflate(R.layout.data_item, null)
            val dataKey: TextView = dataRow.findViewById(R.id.data_key)
            dataKey.text = key
            val dataValue: TextView = dataRow.findViewById(R.id.data_value)
            dataValue.text = info.getStringExtra(key)
            detailContent.addView(dataRow)
        }
    }
}


package com.example.notification_hubs_sample_app_kotlin.cookbook

/**
 * Examples that are ready for copy/paste usage of how to get particular data about the device this
 * application is running on.
 */
object DevicePropertyProvider {
    /**
     * Returns a tag that is normalized across platforms, to allow targeting of any device in a
     * particular country.
     *
     * @return A string that can be used to target devices in a country.
     */
    val countryTag: String
        get() = "Country_" + java.util.Locale.getDefault().getCountry()

    /**
     * Returns a tag that is normalized across platforms, to allow targeting of any device where the
     * user locale is set to a particular language.
     * @return A string that can be used to target device using a language.
     */
    val languageTag: String
        get() {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                return "Language_" + java.util.Locale.getDefault().toLanguageTag()
            }
            throw java.lang.UnsupportedOperationException("Language tag logic was added to the Android standard library in API Level 21 (Lollipop)")
        }

    /**
     * Returns a tag that is normalized across platforms to allow targeting of any device connected
     * to a particular mobile carrier's network.
     * @param context The Application context.
     * @return A string that can be used to target devices on a particular mobile network.
     */
    fun getCarrierTag(context: android.content.Context): String {
        val manager: android.telephony.TelephonyManager = context.getSystemService(android.content.Context.TELEPHONY_SERVICE) as android.telephony.TelephonyManager
        val carrierName: String = manager.getNetworkOperatorName()
        return "MobileCarrier_$carrierName"
    }

    /**
     * Returns a tag that allows for the targeting to all devices made by a particular company.
     * @param context The Application context.
     * @return A string that can be used to target devices manufactured by a given company.
     */
    fun getOemTag(context: android.content.Context?): String {
        return "Oem_" + android.os.Build.MANUFACTURER
    }

    /**
     * Returns a tag that is normalized across platforms to allow for the targeting of any device
     * with a specific screen-size.
     *
     * The resolution that is used will reflect the device's screen resolution minus any permanent
     * graphical elements. Further, width and height will reflect the devices' un-rotated position.
     *
     * @param context The Application context.
     * @return A string that contains the screen dimensions of the current device.
     */
    fun getScreenSizeTag(context: android.content.Context): String {
        val wm: android.view.WindowManager = context.getSystemService(android.content.Context.WINDOW_SERVICE) as android.view.WindowManager
        val disp: android.view.Display = wm.getDefaultDisplay()
        val resolution: android.graphics.Point = android.graphics.Point()
        disp.getSize(resolution)
        when (disp.getRotation()) {
            android.view.Surface.ROTATION_90, android.view.Surface.ROTATION_270 -> resolution.set(resolution.y, resolution.x)
            android.view.Surface.ROTATION_0, android.view.Surface.ROTATION_180 -> {
            }
            else -> throw java.lang.IllegalStateException("Screen-size can only be determined when screen is rotated at a right-angle")
        }
        val builder: java.lang.StringBuilder = java.lang.StringBuilder("ScreenSize_")
        builder.append(resolution.x)
        builder.append('X')
        builder.append(resolution.y)
        return builder.toString()
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.notification_hubs_sample_app_kotlin.R
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationMessage
import java.util.ArrayList

class NotificationDisplayAdapter @kotlin.jvm.JvmOverloads constructor(initialList: List<NotificationMessage> = ArrayList(0)) : RecyclerView.Adapter<NotificationDisplayAdapter.ViewHolder?>() {
    private var mNotifications: List<NotificationMessage>
    private var mClickListener: NotificationClickListener

    /**
     * Called when RecyclerView needs a new [ViewHolder] of the given type to represent
     * an item.
     *
     *
     * This new ViewHolder should be constructed with a new View that can represent the items
     * of the given type. You can either create a new View manually or inflate it from an XML
     * layout file.
     *
     *
     * The new ViewHolder will be used to display items of the adapter using
     * [.onBindViewHolder]. Since it will be re-used to display
     * different items in the data set, it is a good idea to cache references to sub views of
     * the View to avoid unnecessary [View.findViewById] calls.
     *
     * @param parent   The ViewGroup into which the new View will be added after it is bound to
     * an adapter position.
     * @param viewType The view type of the new View.
     * @return A new ViewHolder that holds a View of the given view type.
     * @see .getItemViewType
     * @see .onBindViewHolder
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v: View = LayoutInflater.from(parent.context).inflate(R.layout.notification_list_item, parent, false)
        return ViewHolder(v)
    }

    fun setNotifications(notifications: List<NotificationMessage>) {
        mNotifications = notifications
        notifyDataSetChanged()
    }

    fun setClickListener(listener: NotificationClickListener) {
        mClickListener = listener
    }

    /**
     * Called by RecyclerView to display the data at the specified position. This method should
     * update the contents of the [ViewHolder.itemView] to reflect the item at the given
     * position.
     *
     *
     * Note that unlike [ListView], RecyclerView will not call this method
     * again if the position of the item changes in the data set unless the item itself is
     * invalidated or the new position cannot be determined. For this reason, you should only
     * use the `position` parameter while acquiring the related data item inside
     * this method and should not keep a copy of it. If you need the position of an item later
     * on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
     * have the updated adapter position.
     *
     *
     * Override [.onBindViewHolder] instead if Adapter can
     * handle efficient partial bind.
     *
     * @param holder   The ViewHolder which should be updated to represent the contents of the
     * item at the given position in the data set.
     * @param position The position of the item within the adapter's data set.
     */
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val entry: NotificationMessage = mNotifications[position]
        holder.mTitle.text = entry.title
        holder.mBody.text = entry.body
        holder.mDataCardinality.text = entry.data.size.toString()
    }

    /**
     * Returns the total number of items in the data set held by the adapter.
     *
     * @return The total number of items in this adapter.
     */

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val mTitle: TextView = itemView.findViewById(R.id.titleValue) as TextView
        val mBody: TextView = itemView.findViewById(R.id.bodyValue) as TextView
        val mDataCardinality: TextView = itemView.findViewById(R.id.dataCardinalityValue) as TextView

        init {
            itemView.setOnClickListener {
                val clicked: NotificationMessage = mNotifications[adapterPosition]
                mClickListener.onNotificationClicked(clicked)
            }
        }
    }

    interface NotificationClickListener {
        fun onNotificationClicked(message: NotificationMessage?)
    }

    init {
        mNotifications = initialList
        mClickListener = object : NotificationClickListener {
            override fun onNotificationClicked(message: NotificationMessage?) {

            }
        }
    }

    override fun getItemCount(): Int {
        return mNotifications.size
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.notification_hubs_sample_app_kotlin.NotificationDetailActivity
import com.example.notification_hubs_sample_app_kotlin.R
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationMessage

class NotificationListFragment : Fragment() {
    private var mViewModel: NotificationListViewModel? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewModel = ViewModelProvider(this).get(NotificationListViewModel::class.java)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val root: View = inflater.inflate(R.layout.notification_list_fragment, container, false)
        val notificationDisplayAdapter = NotificationDisplayAdapter()
        val notificationsObserver: Observer<List<NotificationMessage>> = Observer { notificationMessages: List<NotificationMessage> ->
            notificationDisplayAdapter.setNotifications(notificationMessages)
            Toast.makeText(this.context, R.string.notification_received_message, Toast.LENGTH_SHORT).show()
        }
        mViewModel?.getNotificationList()?.observe(viewLifecycleOwner, notificationsObserver)
        val notificationList: RecyclerView = root.findViewById(R.id.notificationList)
        notificationList.layoutManager = LinearLayoutManager(activity)
        notificationList.adapter = notificationDisplayAdapter
        notificationDisplayAdapter.setClickListener(object : NotificationDisplayAdapter.NotificationClickListener {
            override fun onNotificationClicked(message: NotificationMessage?) {
                val i = Intent(activity, NotificationDetailActivity::class.java)
                if (message != null) {
                    i.putExtra(NotificationDetailActivity.INTENT_TITLE_KEY, message.title)
                }
                if (message != null) {
                    i.putExtra(NotificationDetailActivity.INTENT_BODY_KEY, message.body)
                }
                if (message != null) {
                    for ((key, value) in message.data.entries) {
                        i.putExtra(key, value)
                    }
                }
                startActivity(i)
            }
        })
        return root
    }

    companion object {
        fun newInstance(): NotificationListFragment {
            return NotificationListFragment()
        }
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationHub
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationListener
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationMessage

class NotificationListViewModel : ViewModel() {
    private val mNotifications: MutableLiveData<ArrayList<NotificationMessage>> = MutableLiveData()
    private val mListener: NotificationListener
    fun addNotification(notification: NotificationMessage?) {
        var toUpdate: ArrayList<NotificationMessage>? = mNotifications.value
        if (toUpdate == null) {
            toUpdate = ArrayList()
        }
        if (notification != null) {
            toUpdate.add(0, notification)
        }
        mNotifications.postValue(toUpdate)
    }

    fun clearNotifications() {
        var toUpdate: ArrayList<NotificationMessage>? = mNotifications.value
        if (toUpdate == null) {
            toUpdate = ArrayList()
        } else {
            toUpdate.clear()
        }
        mNotifications.postValue(toUpdate)
    }

    fun getNotificationList(): MutableLiveData<ArrayList<NotificationMessage>> {
        return mNotifications;
    }

    init {
        mListener = NotificationListener { context: Context?, message: NotificationMessage? -> addNotification(message) }
        NotificationHub.setListener(mListener)
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel

class PageViewModel : ViewModel() {
    private val mIndex: MutableLiveData<Int> = MutableLiveData()
    private val mText: LiveData<String?> = Transformations.map(mIndex) { input -> "Hello world from section: $input" }

    fun setIndex(index: Int) {
        mIndex.value = index
    }

    fun getText(): LiveData<String?>? {
        return mText
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.example.notification_hubs_sample_app_kotlin.R

/**
 * A placeholder fragment containing a simple view.
 */
class PlaceholderFragment : Fragment() {
    private var pageViewModel: PageViewModel? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pageViewModel = ViewModelProviders.of(this).get(PageViewModel::class.java)
        var index = 1
        if (arguments != null) {
            index = arguments!!.getInt(ARG_SECTION_NUMBER)
        }
        pageViewModel!!.setIndex(index)
    }

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
        val root: View = inflater.inflate(R.layout.fragment_main, container, false)
        val textView: TextView = root.findViewById(R.id.section_label)
        pageViewModel?.getText()?.observe(viewLifecycleOwner, Observer { s -> textView.text = s })
        return root
    }

    companion object {
        private const val ARG_SECTION_NUMBER = "section_number"
        fun newInstance(index: Int): PlaceholderFragment {
            val fragment = PlaceholderFragment()
            val bundle = Bundle()
            bundle.putInt(ARG_SECTION_NUMBER, index)
            fragment.arguments = bundle
            return fragment
        }
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import com.example.notification_hubs_sample_app_kotlin.R

/**
 * A [FragmentPagerAdapter] that returns a fragment corresponding to
 * one of the sections/tabs/pages.
 */
class SectionsPagerAdapter(context: Context, fm: FragmentManager?) : FragmentPagerAdapter(fm!!) {
    private val mContext: Context = context
    override fun getItem(position: Int): Fragment {
        return when (position) {
            0 -> SetupFragment()
            1 -> NotificationListFragment()
            else -> PlaceholderFragment.newInstance(position + 1)
        }
    }

    override fun getCount(): Int {
        return 2;
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return mContext.resources.getString(TAB_TITLES[position])
    }

    companion object {
        @androidx.annotation.StringRes
        private val TAB_TITLES = intArrayOf(R.string.tab_text_1, R.string.tab_text_2)
    }

}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.notification_hubs_sample_app_kotlin.R
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationHub

class SetupFragment : androidx.fragment.app.Fragment() {
    private var mViewModel: SetupViewModel? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewModel = ViewModelProvider(this).get(SetupViewModel::class.java)
        val unknownText: String = resources.getString(R.string.unknown_information)
        mViewModel!!.setUnknownText(unknownText)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val root: View = inflater.inflate(R.layout.setup_fragment, container, false)
        val deviceTokenValue: TextView = root.findViewById(R.id.device_token_value)
        mViewModel?.getDeviceToken()?.observe(viewLifecycleOwner, Observer { text: CharSequence? -> deviceTokenValue.text = text })
        val installationIdValue: TextView = root.findViewById(R.id.installation_id_value)
        mViewModel?.getInstallationId()?.observe(viewLifecycleOwner, Observer { text: CharSequence? -> installationIdValue.text = text })
        val isEnabled: Switch = root.findViewById(R.id.enabled_switch)
        mViewModel?.getIsEnabled()?.observe(viewLifecycleOwner, Observer { checked: Boolean? ->
            if (checked != null) {
                isEnabled.isChecked = checked
            }
        })
        isEnabled.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> mViewModel?.setIsEnabled(isChecked) }
        val tagToAddField: EditText = root.findViewById(R.id.add_tag_field)
        val tagToAddButton: Button = root.findViewById(R.id.add_tag_button)
        tagToAddButton.setOnClickListener {
            try {
                mViewModel?.addTag(tagToAddField.text.toString())
            } catch (e: java.lang.IllegalArgumentException) {
                val toast: Toast = Toast.makeText(context, R.string.invalid_tag_message, Toast.LENGTH_SHORT)
                toast.show()
            }
            tagToAddField.text.clear()
        }
        val tagDisplayAdapter = mViewModel?.let { TagDisplayAdapter(it) }
        val tagsObserver: Observer<List<String>> = Observer { s: List<String>? ->
            tagDisplayAdapter?.setTags(s)
        }
        mViewModel?.getTags()?.observe(viewLifecycleOwner, tagsObserver)
        val tagList: RecyclerView = root.findViewById(R.id.tag_list)
        tagList.layoutManager = LinearLayoutManager(activity)
        tagList.adapter = tagDisplayAdapter
        NotificationHub.setInstallationSavedListener {
            Toast.makeText(this.context, getString(R.string.installation_saved_message), Toast.LENGTH_SHORT).show()
            mViewModel?.setInstallationId(NotificationHub.getInstallationId())
            mViewModel?.setDeviceToken(NotificationHub.getPushChannel())
        }
        NotificationHub.setInstallationSaveFailureListener { Toast.makeText(this.context, getString(R.string.installation_save_failure_message), Toast.LENGTH_SHORT).show() }
        return root
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.microsoft.windowsazure.messaging.notificationhubs.NotificationHub
import java.util.regex.Pattern

class SetupViewModel : ViewModel() {
    private var mUnknownText: String? = null
    private val mDeviceToken: MutableLiveData<String> = MutableLiveData()
    private val mInstallationId: MutableLiveData<String> = MutableLiveData()
    private val mTags: MutableLiveData<List<String>> = MutableLiveData()
    private val mIsEnabled: MutableLiveData<Boolean> = MutableLiveData()

    fun getDeviceToken(): LiveData<String?>? {
        return mDeviceToken
    }

    fun setDeviceToken(deviceToken: String?) {
        mDeviceToken.value = deviceToken
    }

    fun getInstallationId(): LiveData<String?>? {
        return mInstallationId
    }

    fun setInstallationId(installationId: String?) {
        NotificationHub.setInstallationId(installationId)
        mInstallationId.value = installationId
    }

    fun getIsEnabled(): LiveData<Boolean?>? {
        return mIsEnabled
    }

    fun setIsEnabled(b: Boolean) {
        NotificationHub.setEnabled(b)
        mIsEnabled.value = NotificationHub.isEnabled()
    }

    fun getTags(): LiveData<List<String>> {
        return mTags
    }

    fun addTag(tag: String?) {
        if (!isAllowableTag(tag)) {
            throw java.lang.IllegalArgumentException()
        }
        if (NotificationHub.addTag(tag)) {
            mTags.postValue(iterableToList<String>(NotificationHub.getTags()))
        }
    }

    fun removeTag(tag: String?) {
        if (NotificationHub.removeTag(tag)) {
            mTags.postValue(iterableToList<String>(NotificationHub.getTags()))
        }
    }

    fun getUnknownText(): String? {
        return if (mUnknownText == null) {
            ""
        } else mUnknownText
    }

    fun setUnknownText(unknownText: String) {
        mUnknownText = unknownText
    }

    companion object {
        private val sTagPattern: Pattern = Pattern.compile("^[a-zA-Z0-9_@#\\.:\\-]{1,120}$")
        fun <T> iterableToList(iterable: Iterable<T>): List<T> {
            val retval: ArrayList<T> = ArrayList()
            for (entry in iterable) {
                retval.add(entry)
            }
            return retval
        }

        fun isAllowableTag(tag: String?): Boolean {
            return sTagPattern.matcher(tag).matches()
        }
    }

    init {
        mTags.value = iterableToList<String>(NotificationHub.getTags())
        mIsEnabled.value = NotificationHub.isEnabled()
        val pushChannel: String? = NotificationHub.getPushChannel()
        if (pushChannel != null) {
            mDeviceToken.value = pushChannel
        }
        val installationId: String? = NotificationHub.getInstallationId()
        if (installationId != null) {
            mInstallationId.value = installationId
        }
    }
}


package com.example.notification_hubs_sample_app_kotlin.ui.main

import android.view.LayoutInflater
import com.example.notification_hubs_sample_app_kotlin.R
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class TagDisplayAdapter(viewModel: SetupViewModel) : RecyclerView.Adapter<TagDisplayAdapter.ViewHolder?>() {
    private var mTags: List<String>? = null
    private val mViewModel: SetupViewModel = viewModel

    /**
     * Called when RecyclerView needs a new [androidx.recyclerview.widget.RecyclerView.ViewHolder] of the given type to represent
     * an item.
     *
     *
     * This new ViewHolder should be constructed with a new View that can represent the items
     * of the given type. You can either create a new View manually or inflate it from an XML
     * layout file.
     *
     *
     * The new ViewHolder will be used to display items of the adapter using
     * [.onBindViewHolder]. Since it will be re-used to display
     * different items in the data set, it is a good idea to cache references to sub views of
     * the View to avoid unnecessary [View.findViewById] calls.
     *
     * @param parent   The ViewGroup into which the new View will be added after it is bound to
     * an adapter position.
     * @param viewType The view type of the new View.
     * @return A new ViewHolder that holds a View of the given view type.
     * @see .getItemViewType
     * @see .onBindViewHolder
     */
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val v: View = LayoutInflater.from(parent.context).inflate(R.layout.tag_row_item, parent, false)
        return ViewHolder(v)
    }

    /**
     * Called by RecyclerView to display the data at the specified position. This method should
     * update the contents of the [androidx.recyclerview.widget.RecyclerView.ViewHolder.itemView] to reflect the item at the given
     * position.
     *
     *
     * Note that unlike [ListView], RecyclerView will not call this method
     * again if the position of the item changes in the data set unless the item itself is
     * invalidated or the new position cannot be determined. For this reason, you should only
     * use the `position` parameter while acquiring the related data item inside
     * this method and should not keep a copy of it. If you need the position of an item later
     * on (e.g. in a click listener), use [androidx.recyclerview.widget.RecyclerView.ViewHolder.getAdapterPosition] which will
     * have the updated adapter position.
     *
     *
     * Override [.onBindViewHolder] instead if Adapter can
     * handle efficient partial bind.
     *
     * @param holder   The ViewHolder which should be updated to represent the contents of the
     * item at the given position in the data set.
     * @param position The position of the item within the adapter's data set.
     */
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.tagText.text = mTags!![position]
    }

    /**
     * Returns the total number of items in the data set held by the adapter.
     *
     * @return The total number of items in this adapter.
     */
    override fun getItemCount(): Int {
        return if (mTags == null) 0 else mTags!!.size;
    }

    fun setTags(tags: List<String>?) {
        mTags = tags
        notifyDataSetChanged()
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val mTagText: TextView = itemView.findViewById(R.id.tag_entry_value) as TextView
        private val mDeleteButton: ImageButton = itemView.findViewById(R.id.tag_delete_button) as ImageButton
        val tagText: TextView
            get() = mTagText

        val deleteButton: ImageButton
            get() = mDeleteButton

        init {
            mDeleteButton.setOnClickListener { mViewModel.removeTag(mTagText.text.toString()) }
        }
    }

}


package com.example.notification_hubs_sample_app_kotlin

import org.junit.Test

import org.junit.Assert.*

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}