BluetoothLeScanner — cos'è, metodi e scansione BLE in Android

Autore: IT Sectr Pubblicato: 2026-07-16 Tempo di lettura: 10 min

BluetoothLeScanner è una classe Android per scansionare dispositivi Bluetooth Low Energy, disponibile dall'API 21 (Android 5.0). BluetoothLeScanner ha sostituito il metodo obsoleto startLeScan su BluetoothAdapter, fornendo un'API flessibile con configurazione di scansione (ScanSettings), filtraggio (ScanFilter) e supporto per la modalità background (PendingIntent). L'istanza si ottiene tramite BluetoothAdapter.getBluetoothLeScanner(). Secondo Android Developers, 2026, BluetoothLeScanner supporta tre modalità di alimentazione e consente di scansionare pacchetti pubblicitari BLE con filtraggio per UUID del servizio, nome del dispositivo o indirizzo MAC.

Punti Chiave

  • BluetoothLeScanner — API Android moderna (API 21+) per scansione BLE, sostituzione del obsoleto startLeScan
  • ScanSettings — configurazione della modalità di scansione: LOW_POWER, BALANCED, LOW_LATENCY e tipo di callback
  • ScanFilter — filtraggio dei risultati per UUID del servizio, nome dispositivo, indirizzo MAC, dati produttore
  • ScanCallback — callback dei risultati onScanResult, onBatchScanResults e onScanFailed con codici di errore
  • PendingIntent — scansione in background tramite BroadcastReceiver, anche quando l'app è in background

Cos'è BluetoothLeScanner: essenza e ottenimento dell'istanza

BluetoothLeScanner è una classe di sistema per gestire la scansione BLE su Android. A differenza di BluetoothAdapter.startLeScan(), che accetta un semplice LeScanCallback, BluetoothLeScanner fornisce un'API orientata agli oggetti con impostazioni, filtri e gestione avanzata degli errori. Questa classe è stata introdotta nell'API 21 (Android 5.0) insieme al supporto BLE 4.2 e rimane il metodo principale di scansione BLE su tutte le versioni moderne di Android.

L'ottenimento di un'istanza BluetoothLeScanner avviene tramite BluetoothAdapter.getBluetoothLeScanner(). Il metodo restituisce null se l'adattatore Bluetooth non è disponibile (Bluetooth disattivato o dispositivo non supporta BLE). Prima di ottenerlo, verificare BluetoothAdapter.isEnabled() e la presenza di FEATURE_BLUETOOTH_LE tramite PackageManager. Una volta ottenuto lo scanner, la scansione può essere avviata su qualsiasi thread — Android pianifica le operazioni BLE su un thread interno dello stack Bluetooth.

kotlin
// Ottenere BluetoothLeScanner
class BLEScannerManager(context: Context) {

    private val bluetoothManager: BluetoothManager =
        context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    private val adapter: BluetoothAdapter? = bluetoothManager.adapter
    private var scanner: BluetoothLeScanner? = null

    fun initScanner(): Boolean {
        // Verificare disponibilità BLE
        if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            return false
        }

        // Verificare Bluetooth attivato
        if (adapter?.isEnabled != true) {
            return false
        }

        // Ottenere scanner
        scanner = adapter?.bluetoothLeScanner
        return scanner != null
    }

    // Verificare disponibilità scanner
    val isAvailable: Boolean
        get() = scanner != null

    // Avviare scansione base senza filtri
    fun startBasicScan() {
        scanner?.startScan(object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                handleResult(result)
            }
        })
    }

    private fun handleResult(result: ScanResult) {
        val device = result.device
        print("Device: ${device.name ?: "Unnamed"}, RSSI: ${result.rssi}, address: ${device.address}")
    }
}

La classe BLEScannerManager dimostra l'ottenimento e l'inizializzazione sicuri di BluetoothLeScanner. initScanner verifica la disponibilità BLE tramite hasSystemFeature, Bluetooth attivato e ottenimento riuscito dello scanner. startBasicScan avvia la scansione senza impostazioni o filtri — rileva tutti i dispositivi BLE nel raggio d'azione. handleResult analizza ScanResult: BluetoothDevice (nome, indirizzo), RSSI (intensità del segnale), scanRecord (dati pubblicitari).

ScanSettings: modalità di scansione e tipo di callback

ScanSettings è una classe per la configurazione della scansione BLE. Il parametro principale è la modalità di scansione (scanMode), che determina il compromesso tra consumo energetico e latenza di rilevamento. ScanSettings.Builder consente di configurare: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (ritardo di consegna batch) e phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).

Tre modalità di scansione: SCAN_MODE_LOW_POWER (0) — scansione in background con basso consumo, ritardo di rilevamento di diversi secondi. SCAN_MODE_BALANCED (1) — modalità bilanciata per la maggior parte degli scenari. SCAN_MODE_LOW_LATENCY (2) — latenza di rilevamento minima (circa 100 ms), consumo massimo. Per la ricerca attiva di dispositivi utilizzare LOW_LATENCY, per il monitoraggio in background utilizzare LOW_POWER.

reportDelay — ritardo in millisecondi prima della consegna batch dei risultati. Se reportDelay = 0, i risultati vengono inviati immediatamente al rilevamento. Se > 0, Android accumula i risultati e invia un batch tramite onBatchScanResults. La consegna batch riduce le invocazioni di callback e il consumo energetico, adatta per scansione in background a bassa priorità.

kotlin
// Configurazione ScanSettings per diversi scenari
class ScanSettingsProvider {

    // 1. Scansione rapida (ricerca attiva)
    fun lowLatencyScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
            .setReportDelay(0)
            .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
            .build()
    }

    // 2. Scansione efficiente (monitoraggio background)
    fun lowPowerScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
            .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
            .setReportDelay(2000)  // batch ogni 2 secondi
            .build()
    }

    // 3. Scansione BLE Long Range (Coded PHY)
    fun longRangeScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setPhy(ScanSettings.PHY_LE_CODED)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .build()
    }

    // 4. Scansiona solo su 2M PHY (BLE 5.0+)
    fun highSpeedScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setPhy(ScanSettings.PHY_LE_2M)
            .build()
    }
}

La classe ScanSettingsProvider contiene configurazioni tipiche. lowLatencyScan — per scansione UI (ricerca “qui e ora”). lowPowerScan — per monitoraggio in background con batch ogni 2 secondi e callbackType FIRST_MATCH (si attiva solo al primo rilevamento). longRangeScan utilizza PHY_LE_CODED (BLE Long Range, fino a 1 km). highSpeedScan — PHY_LE_2M (2 Mbit/s, solo dispositivi BLE 5.0+).

ScanFilter: filtraggio dispositivi BLE per UUID e nome

ScanFilter è una classe per filtrare i risultati della scansione BLE. Senza filtro, BluetoothLeScanner restituisce tutti i dispositivi BLE nel raggio — in un ambiente BLE denso possono essere centinaia di pacchetti al minuto. ScanFilter restringe i risultati ai dispositivi desiderati, riducendo il consumo energetico e il carico dell'app. I filtri vengono applicati a livello dello stack Bluetooth — i pacchetti non adatti vengono scartati prima di raggiungere l'app.

Tipi di filtri: setServiceUuid — UUID del servizio (formato completo a 128 bit richiesto). setDeviceName — sottostringa del nome del dispositivo (sensibile a maiuscole/minuscole, corrispondenza esatta di sottostringa). setDeviceAddress — indirizzo MAC esatto. setManufacturerData — dati del produttore (ID azienda + maschera). È possibile impostare più filtri per una singola scansione — il dispositivo deve corrispondere a tutti (logica AND). Per logica OR, avviare più scansioni.

kotlin
// Creazione ScanFilter per diversi scenari
class ScanFilterFactory {

    // 1. Filtra per UUID servizio (Monitor frequenza cardiaca)
    fun byHeartRateService(): ScanFilter {
        return ScanFilter.Builder()
            .setServiceUuid(
                ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
            )
            .build()
    }

    // 2. Filtra per nome dispositivo ( "iBeacon*")
    fun byDeviceName(): ScanFilter {
        return ScanFilter.Builder()
            .setDeviceName("Sensor")
            .build()
    }

    // 3.   MAC- ( dispositivi)
    fun byMacAddress(mac: String): ScanFilter {
        return ScanFilter.Builder()
            .setDeviceAddress(mac)
            .build()
    }

    // 4. Filtro combinato (UUID + )
    fun combinedFilter(): List<ScanFilter> {
        return listOf(
            ScanFilter.Builder()
                .setServiceUuid(
                    ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
                )
                .setDeviceName("MyDevice")
                .build()
        )
    }

    // 5.   dati produttore
    fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
        return ScanFilter.Builder()
            .setManufacturerData(companyId, data, mask)
            .build()
    }
}

La classe ScanFilterFactory mostra tutti i tipi di filtro. byHeartRateService filtra i dispositivi con il servizio frequenza cardiaca 0x180D. byDeviceName trova dispositivi contenenti “Sensor” nel nome (Apple consiglia nomi univoci per il filtraggio). byMacAddress — ricerca esatta di un dispositivo specifico. combinedFilter — filtro AND per UUID e nome. byManufacturer — filtro per dati del produttore (ad esempio, per iBeacon viene utilizzato l'ID azienda Apple 0x004C).

ScanCallback: elaborazione risultati e errori di scansione

ScanCallback è una classe astratta per ricevere i risultati della scansione BLE. Contiene tre metodi: onScanResult — risultato singolo (tipo di callback, ScanResult), onBatchScanResults — risultati batch per reportDelay > 0, onScanFailed — codice di errore. Tutti i metodi vengono chiamati sul thread principale di Android. Per elaborazioni lunghe in onScanResult, utilizzare coroutine o HandlerThread.

ScanResult contiene: BluetoothDevice device, int rssi (livello del segnale in dBm), ScanRecord scanRecord (dati pubblicitari), long timestampNanos (tempo di rilevamento dall'avvio del sistema). ScanRecord fornisce: getServiceData() — UUID + dati personalizzati, getManufacturerSpecificData() — dati del produttore, getAdvertiseFlags() — flag BLE. Il tipo di callback indica: CALLBACK_TYPE_ALL_MATCHES — corrispondenza con il filtro, CALLBACK_TYPE_FIRST_MATCH — primo rilevamento, CALLBACK_TYPE_MATCH_LOST — dispositivo perso.

Codici di errore onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — scansione già avviata, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — registrazione dell'app nello stack Bluetooth fallita, SCAN_FAILED_INTERNAL_ERROR (3) — errore interno dello stack, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — scansione BLE non supportata sul dispositivo.

kotlin
// Risultati scansione completi e gestione errori
class ScanResultHandler {

    private val results = mutableListOf<ScanResult>()

    val scanCallback = object : ScanCallback() {

        // 1. Risultato singolo
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            // callbackType: 1 = ALL_MATCHES, 2 = FIRST_MATCH, 4 = MATCH_LOST
            if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
                onDeviceLost(result)
                return
            }

            // Aggiungi a elenco (dedup per indirizzo)
            val existingIndex = results.indexOfFirst {
                it.device.address == result.device.address
            }
            if (existingIndex >= 0) {
                results[existingIndex] = result  // aggiorna RSSI
            } else {
                results.add(result)
            }

            // Estrai dati dal pacchetto pubblicitario
            val record = result.scanRecord
            val serviceData = record?.serviceData
            val manufacturerData = record?.manufacturerSpecificData

            print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
        }

        // 2. Risultati batch (reportDelay > 0)
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            results?.let { batch ->
                print("Batch: ${batch.size} devices")
            }
        }

        // 3. Errore scansione
        override fun onScanFailed(errorCode: Int) {
            val error = when (errorCode) {
                ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Already scanning"
                ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Registration failed"
                ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
                ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "BLE not supported"
                else -> "Unknown error: $errorCode"
            }
            print("Error: $error")
        }
    }

    private fun onDeviceLost(result: ScanResult) {
        results.removeAll { it.device.address == result.device.address }
        print("Device lost: ${result.device.address}")
    }
}

La classe ScanResultHandler gestisce tutti i tipi di callback di BluetoothLeScanner. onScanResult aggiorna l'elenco dei dispositivi con deduplicazione per indirizzo MAC — RSSI viene aggiornato per i dispositivi già trovati. CALLBACK_TYPE_MATCH_LOST segnala la perdita del dispositivo (rimozione dall'elenco). onBatchScanResults elabora i risultati batch per reportDelay > 0. onScanFailed mappa i codici di errore in messaggi leggibili — critico per il debug della scansione BLE.

PendingIntent: scansione BLE in background tramite BroadcastReceiver

Scansione PendingIntent è un meccanismo di BluetoothLeScanner per la scansione BLE che funziona anche quando l'app è in background (con restrizioni Android 8+). Invece di ScanCallback, viene utilizzato un PendingIntent che invia un Broadcast al BroadcastReceiver di sistema al rilevamento di un dispositivo BLE. Ciò consente all'app di ricevere notifiche di dispositivi BLE senza rimanere in memoria (il sistema crea il processo alla ricezione del broadcast).

Limitazioni della scansione in background: Su Android 8+ (API 26), i servizi in background sono limitati — la scansione PendingIntent aggira questa limitazione tramite BroadcastReceiver, che il sistema può avviare alla ricezione di un evento BLE. Su Android 10+ (API 29), la scansione BLE in background è ulteriormente limitata dalle politiche di risparmio energetico dei produttori (Xiaomi, Huawei, Samsung bloccano le operazioni BLE in background). Per scenari BLE critici, è richiesta una notifica di foreground service.

kotlin
// Scansione BLE in background tramite PendingIntent
class BackgroundBLEScanner(private val context: Context) {

    private val scanner: BluetoothLeScanner? by lazy {
        val adapter = BluetoothAdapter.getDefaultAdapter()
        adapter?.bluetoothLeScanner
    }

    fun startBackgroundScan() {
        // Crea PendingIntent per BroadcastReceiver
        val intent = Intent(context, BLEBroadcastReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        // Impostazioni scansione background
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
            .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
            .build()

        // Avvia scansione background
        scanner?.startScan(
            null,  // filtri
            settings,
            pendingIntent
        )
    }

    fun stopBackgroundScan() {
        val intent = Intent(context, BLEBroadcastReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        scanner?.stopScan(pendingIntent)
    }
}

// BroadcastReceiver   BLE-
class BLEBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // Ottieni risultati scansione
        val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
        results?.let { scanResults ->
            for (result in scanResults) {
                // Invia notifica all'utente
                showNotification(context, result.device.name ?: " ")
            }
        }
    }

    private fun showNotification(context: Context, name: String) {
        val notification = Notification.Builder(context, "ble_channel")
            .setSmallIcon(android.R.drawable.ic_dialog_info)
            .setContentTitle("BLE devices")
            .setContentText("Found: $name")
            .setAutoCancel(true)
            .build()
        val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
                as NotificationManager
        manager.notify(System.currentTimeMillis().toInt(), notification)
    }
}

La classe BackgroundBLEScanner avvia la scansione BLE in background tramite PendingIntent. startBackgroundScan crea un PendingIntent che, al rilevamento di un dispositivo BLE, invia un Broadcast a BLEBroadcastReceiver. Il BroadcastReceiver estrae ScanResult tramite getPendingIntentScanResults() e può mostrare una notifica o inviare dati al server. Questo approccio funziona anche se l'app è stata terminata dal sistema — Android riavvia il BroadcastReceiver alla ricezione del broadcast.

Esempio scanner BLE in Kotlin con BluetoothLeScanner

Esempio completo di uno scanner BLE in Kotlin che utilizza BluetoothLeScanner con ScanSettings, ScanFilter e ScanCallback per trovare dispositivi Heart Rate Monitor. Lo scanner mostra un elenco dei dispositivi trovati con RSSI e UUID dei servizi, con possibilità di connettersi tramite BluetoothGatt.

kotlin
// Scanner BLE completo con coroutine in Kotlin
class DeviceScanner(private val context: Context) {

    private val adapter: BluetoothAdapter? by lazy {
        val manager = context.getSystemService(Context.BLUETOOTH_SERVICE)
                as BluetoothManager
        manager.adapter
    }

    private val scanner: BluetoothLeScanner? by lazy {
        adapter?.bluetoothLeScanner
    }

    fun startScan(duration: Long = 10000): Flow<ScanResult> = callbackFlow {
        // Controlla stato Bluetooth
        if (adapter?.isEnabled != true) {
            close(IllegalStateException("Bluetooth disabled"))
            return@callbackFlow
        }

        // Configurazione scansione
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()

        val filters = listOf(
            ScanFilter.Builder()
                .setServiceUuid(ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
                .build()
        )

        val callback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                trySend(result)
            }

            override fun onScanFailed(errorCode: Int) {
                close(BLEException("Scan failed: $errorCode"))
            }
        }

        // Avvia scansione
        scanner?.startScan(filters, settings, callback)

        // Arresto automatico dopo durata
        delay(duration)
        scanner?.stopScan(callback)
        close()
    }.flowOn(Dispatchers.IO)

    fun stop() {
        scanner?.stopScan(object : ScanCallback() {})
    }
}

class BLEException(message: String) : Exception(message)

La classe DeviceScanner utilizza Kotlin Flow (callbackFlow) per la scansione BLE reattiva. La scansione viene avviata con impostazioni LOW_LATENCY e un filtro UUID Heart Rate Service. I risultati vengono emessi tramite onScanResult in un Flow. Arresto automatico dopo una durata specificata (10 secondi predefiniti). FlowOn(Dispatchers.IO) scarica le operazioni BLE su un thread in background. Questo approccio consente la scansione BLE nell'architettura MVVM tramite viewModelScope.launch e collect.

Domande Frequenti

Cos'è BluetoothLeScanner in Android?

BluetoothLeScanner è una classe Android (API 21+) per la scansione BLE. Si ottiene tramite BluetoothAdapter.getBluetoothLeScanner(). Supporta tre modalità di scansione (LOW_POWER, BALANCED, LOW_LATENCY), filtraggio per UUID, nome e indirizzo MAC, risultati batch e PendingIntent per scansione in background. Sostituisce il metodo obsoleto BluetoothAdapter.startLeScan().

In cosa differisce LOW_POWER da LOW_LATENCY?

SCAN_MODE_LOW_POWER — modalità background con ritardo di rilevamento di 5–10 secondi, consumo minimo. SCAN_MODE_LOW_LATENCY — modalità attiva con ritardo di circa 100 ms, consumo massimo. SCAN_MODE_BALANCED — compromesso (~2 secondi di ritardo). Utilizzare LOW_LATENCY per scansione UI, LOW_POWER con PendingIntent per monitoraggio in background.

Perché BluetoothLeScanner non trova dispositivi?

Cause: Bluetooth disattivato (verificare adapter.isEnabled), permessi mancanti (BLUETOOTH_SCAN su API 31+, ACCESS_FINE_LOCATION su API 23–30), scanner = null (adattatore non disponibile), dispositivo fuori portata o filtro errato. Verificare anche onScanFailed — il codice di errore indica la causa: SCAN_FAILED_ALREADY_STARTED (1) o SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).

Come scansionare BLE in background su Android?

Utilizzare la versione PendingIntent di startScan() — passare un PendingIntent invece di ScanCallback. Quando viene rilevato un dispositivo BLE, Android invia un Broadcast al BroadcastReceiver, che può essere avviato dal sistema anche se l'app è in background. Per Android 8+, aggiungere il BroadcastReceiver al manifest. Su Android 10+, considerare le restrizioni di risparmio energetico dei produttori.

Quanti dispositivi BLE possono essere rilevati in una scansione?

BluetoothLeScanner non ha limiti sul numero di dispositivi rilevabili — il limite dipende dalla saturazione BLE dell'ambiente. Un ufficio può avere 20–50 dispositivi BLE attivi, un centro commerciale centinaia. Utilizzare ScanFilter (per UUID, nome) per filtrare. Senza filtraggio, elaborare i risultati in modo asincrono — onScanResult può essere chiamato decine di volte al secondo.

Riepilogo

  • BluetoothLeScanner — classe Android moderna (API 21+) per scansione BLE con impostazioni e filtraggio
  • ScanSettings — tre modalità: LOW_POWER (background), BALANCED, LOW_LATENCY (attiva)
  • ScanFilter — filtraggio per UUID servizio, nome dispositivo, indirizzo MAC, dati produttore con logica AND
  • ScanCallback — onScanResult (singolo), onBatchScanResults (batch), onScanFailed (codici errore)
  • PendingIntent — scansione background tramite BroadcastReceiver, funziona con app terminata
  • ScanRecord — dati pubblicitari BLE: serviceData, manufacturerSpecificData, advertiseFlags, TX power level
  • Kotlin Flow — callbackFlow consente di utilizzare BluetoothLeScanner in architettura reattiva con arresto automatico

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche