I have the BillingHandler class that you can see below which I'm using to handle in-app billing related logic using google's billing library v.3. I'm using Koin to create a singleton instance using single { BillingHandler(androidContext()) } in my app's module.
Now my issue occurs when I call the class' doesUserOwnPremium() method from my SettingsFragment which uses settings preferences for displaying a preference to be used as a purchase button. Firstly, I use get() to access the billingHandler instance and then call the method to check whether or not the user owns the premium product. I've already purchased it while testing but when I first navigate to the fragment, the purchasesList in the BillingHandler class is null so this returns false. After clicking the preference and attempting to launch a billing flow, the handler's if(!ownsProduct()) {..} logic in loadSKUs() is called and evaluates to false thus notifying me that I do own it.
Both the loadSKUs() method and the doesUserOwnPremium() method call ownsProduct() at different times and return the above results each time. Why is that? Does it have something to do with initialization?
SettingsFragment.kt:
class SettingsFragment : SharedPreferences.OnSharedPreferenceChangeListener
, PreferenceFragmentCompat() {
private val TAG = SettingsFragment::class.java.simpleName
// Billing library setup
private lateinit var billingHandler:BillingHandler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
purchasePremiumPreference = findPreference(resources.getString(R.string.purchase_premium))!!
purchasePremiumPreference.isEnabled = false // disable until the client is ready
billingHandler = get()
val ownsPremium = billingHandler.doesUserOwnPremium()
Toast.makeText(requireContext(),"owns product = $ownsPremium",Toast.LENGTH_LONG).show()
if(!ownsPremium) {
purchasePremiumPreference.isEnabled = true
purchasePremiumPreference.setOnPreferenceClickListener {
billingHandler.startConnection()
true
}
}
}
}
BillingHandler.kt:
/**
* Singleton class that acts as an abstraction layer on top of the Billing library V.3 by google.
*/
class BillingHandler(private val context: Context) : PurchasesUpdatedListener {
// Billing library setup
private var billingClient: BillingClient
private var skuList:ArrayList<String> = ArrayList()
private val sku = "remove_ads" // the sku to sell
private lateinit var skuDetails: SkuDetails // the details of the sku to sell
private var ownsPremium = false
fun doesUserOwnPremium() : Boolean = ownsPremium
// analytics
private lateinit var firebaseAnalytics: FirebaseAnalytics
init {
skuList.add(sku) // add SKUs to the sku list (only one in this case)
billingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
ownsPremium = ownsProduct()
}
/**
* Attempts to establish a connection to the billing client. If successful,
* it will attempt to load the SKUs for sale and begin a billing flow if needed.
* If the connection fails, it will prompt the user to either retry the connection
* or cancel it.
*/
fun startConnection() {
// start the connection
billingClient.startConnection(object:BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
loadSKUs()
} else {
Toast.makeText(context,"Something went wrong, please try again!",
Toast.LENGTH_SHORT).show()
}
}
override fun onBillingServiceDisconnected() {
Toast.makeText(context,"Billing service disconnected!", Toast.LENGTH_SHORT).show()
TODO("implement retry policy. Maybe using a dialog w/ retry and cancel buttons")
}
})
}
/**
* Loads the skus from the skuList and starts the billing flow
* for the selected sku(s) if needed.
*/
private fun loadSKUs() {
if(billingClient.isReady) { // load the products that the user can purchase
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.querySkuDetailsAsync(skuDetailsParams
) { billingResult, skuDetailsList ->
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null && skuDetailsList.isNotEmpty()) {
// for each sku details object
for(skuDetailsObj in skuDetailsList) {
// make sure the sku we want to sell is in the list and do something for it
if(skuDetailsObj.sku == sku) {
if(!ownsProduct()) { // product not owned
skuDetails = skuDetailsObj // store the details of that sku
startBillingFlow(skuDetailsObj)
} else { // give premium benefits
Toast.makeText(context,"You already own Premium!",Toast.LENGTH_SHORT).show()
}
}
}
}
}
} else {
Toast.makeText(context,"Billing client is not ready. Please try again!",Toast.LENGTH_SHORT).show()
}
}
/**
* Checks whether or not the user owns the desired sku
* #return True if they own the product, false otherwise
*/
private fun ownsProduct(): Boolean {
var ownsProduct = false
// check if the user already owns this product
// query the user's purchases (reads them from google play's cache)
val purchasesResult: Purchase.PurchasesResult =
billingClient.queryPurchases(BillingClient.SkuType.INAPP)
val purchasesList = purchasesResult.purchasesList // get the actual list of purchases
if (purchasesList != null) {
for (purchase in purchasesList) {
if (purchase.sku == sku) {
ownsProduct = true
break
}
}
} else {
Toast.makeText(context,"Purchases list was null",Toast.LENGTH_SHORT).show()
}
return ownsProduct
}
/**
* Starts the billing flow for the purchase of the desired
* product.
* #param skuDetailsObj The SkuDetails object of the selected sku
*/
private fun startBillingFlow(skuDetailsObj:SkuDetails) {
val billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsObj)
.build()
billingClient.launchBillingFlow(
context as Activity,
billingFlowParams
)
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchasesList: MutableList<Purchase>?) {
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
purchasesList != null) {
for(purchase in purchasesList) {
handlePurchase(purchase)
}
}
}
/**
* Handles the given purchase by acknowledging it if needed .
* #param purchase The purchase to handle
*/
private fun handlePurchase(purchase: Purchase) {
// if the user purchased the desired sku
if(purchase.sku == sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if(!purchase.isAcknowledged) { // acknowledge the purchase so that it doesn't get refunded
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgePurchaseParams
) { billingResult ->
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // log the event using firebase
// log event to firebase
val eventBundle = Bundle()
eventBundle.putString(FirebaseAnalytics.Param.ITEM_ID,"purchase_ack")
eventBundle.putString(FirebaseAnalytics.Param.ITEM_NAME,"Purchase acknowledged")
eventBundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, "IN_APP_PURCHASES")
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE,eventBundle)
}
}
}
showPurchaseSuccessDialog()
}
}
/**
* Shows a dialog to inform the user of the successful purchase
*/
private fun showPurchaseSuccessDialog() {
MaterialDialog(context).show {
title(R.string.premium_success_dialog_title)
message(R.string.premium_success_dialog_msg)
icon(R.drawable.ic_premium)
}
}
}
After doing some more digging and reading the docs again, I realized that when I'm first calling the ownsProduct() method, the billing client's startConnection() method hasn't been called yet thus why the query returned null, the client wasn't ready yet.
I decided to bypass that by simply using the following method to begin a dummy connection in order to set up the client from within my Application class. This way, by the time the user gets anywhere in the app, the client is ready and I can get his/hers actual purchase list.
fun dummyConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(p0: BillingResult) {
}
override fun onBillingServiceDisconnected() {
}
})
}
I'm guessing that this could have bad side effects so I'd love to get some feedback on whether this is the right way to go about it or not. By the way, I need the client ready as soon as possible because I want to be able to verify that they own premium throughout the app (to disable ads, etc.).
Related
I try to to use Flow instead of LiveData in repos.
In viewModel:
val state: LiveData<StateModel> = stateRepo
.getStateFlow("euro")
.catch {}
.asLiveData()
Repository:
override fun getStateFlow(currencyCode: String): Flow<StateModel> {
return serieDao.getStateFlow(currencyCode).map {with(stateMapper) { it.fromEntityToDomain() } }
}
It works fine if currCode if always the same during viewModel's lifetime, for example euro
but what to do if currCode is changed to dollar?
How to make state to show a Flow for another param?
You need to switchMap your repository call.
I imagine you could dosomething like this:
class SomeViewModel : ViewModel() {
private val currencyFlow = MutableStateFlow("euro");
val state = currencyFlow.switchMap { currentCurrency ->
// In case they return different types
when (currentCurrency) {
// Assuming all of these database calls return a Flow
"euro" -> someDao.euroCall()
"dollar" -> someDao.dollarCall()
else -> someDao.elseCall()
}
// OR in your case just call
serieDao.getStateFlow(currencyCode).map {
with(stateMapper) { it.fromEntityToDomain() }
}
}
.asLiveData(Dispatchers.IO); //Runs on IO coroutines
fun setCurrency(newCurrency: String) {
// Whenever the currency changes, then the state will emit
// a new value and call the database with the new operation
// based on the neww currency you have selected
currencyFlow.value = newCurrency
}
}
i was wondering if it is possible to send messages from the backend (for example a running task that receives information from an external system) to the UI. In my case it needs to be a specific session (no broadcast) and only on a specific screen
plan B would be polling the backend frequently but i was hoping to get something more "realtime"
I was trying to work something out like this, but i keep getting a NotSerializableException.
#Push
class StorageAccess : Screen(), MessageListener {
#Inject
private lateinit var stationWSService: StationWebSocketService
#Inject
private lateinit var notifications: Notifications
#Subscribe
private fun onInit(event: InitEvent) {
}
#Subscribe("stationPicker")
private fun onStationPickerValueChange(event: HasValue.ValueChangeEvent<StorageUnit>) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
val prevValue = event.prevValue
if (prevValue != null) {
stationWSService.remove(current.userSession.id)
}
val value = event.value ?: return
stationWSService.listen(current.userSession.id, value, this)
}
override fun messageReceived(message: String) {
val current = AppUI.getCurrent()
current.access {
notifications.create().withCaption(message).show()
}
}
#Subscribe
private fun onAfterDetach(event: AfterDetachEvent) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
stationWSService.remove(current.userSession.id)
}
}
-- The callback interface
interface MessageListener : Serializable {
fun messageReceived(message: String);
}
-- The listen method of my backend service
private val listeners: MutableMap<String, MutableMap<UUID, MessageListener>> = HashMap()
override fun listen(id: UUID, storageUnit: StorageUnit, callback: MessageListener) {
val unitStationIP: String = storageUnit.unitStationIP ?: return
if (!listeners.containsKey(unitStationIP))
listeners[unitStationIP] = HashMap()
listeners[unitStationIP]?.set(id, callback)
}
The Exception i get is NotSerializableException: com.haulmont.cuba.web.sys.WebNotifications which happens during adding the listener to the backend: stationWSService.listen(current.userSession.id, value, this)
as far as i understand this is the place where the UI sends the information to the backend - and with it the entire status of the class StorageAccess, including all its members.
is there an elegant solution to this?
regards
There is an add-on that solves exactly this problem: https://github.com/cuba-platform/global-events-addon
I'm attempting to use Kotlin's Flow class as a message queue to transfer data from a producer (a camera) to a set of workers (image analyzers) running on separate coroutines.
The producer in my case is a camera, and will run substantially faster than the workers. Back pressure should be handled by dropping data so that the image analyzers are always operating on the latest images from the camera.
When using channels, this solution works, but seems messy and does not provide an easy way for me to translate the data between the camera and the analyzers (like flow.map).
class ImageAnalyzer<Result> {
fun analyze(image: Bitmap): Result {
// perform some work on the image and return a Result. This can take a long time.
}
}
class CameraAdapter {
private val imageChannel = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
private val imageReceiveMutex = Mutex()
// additional code to make this camera work and listen to lifecycle events of the enclosing activity.
protected fun sendImageToStream(image: CameraOutput) {
// use channel.offer to ensure the latest images are processed
runBlocking { imageChannel.offer(image) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
runBlocking { imageChannel.close() }
}
/**
* Get the stream of images from the camera.
*/
fun getImageStream(): ReceiveChannel<Bitmap> = imageChannel
}
class ImageProcessor<Result>(workers: List<ImageAnalyzer<Result>>) {
private val analysisResults = Channel<Result>(capacity = Channel.RENDEZVOUS)
private val cancelMutex = Mutex()
var finished = false // this can be set elsewhere when enough images have been analyzed
fun subscribeTo(channel: ReceiveChannel<Bitmap>, processingCoroutineScope: CoroutineScope) {
// omit some checks to make sure this is not already subscribed
processingCoroutineScope.launch {
val workerScope = this
workers.forEachIndexed { index, worker ->
launch(Dispatchers.Default) {
startWorker(channel, workerScope, index, worker)
}
}
}
}
private suspend fun startWorker(
channel: ReceiveChannel<Bitmap>,
workerScope: CoroutineScope,
workerId: Int,
worker: ImageAnalyzer
) {
for (bitmap in channel) {
analysisResults.send(worker.analyze(bitmap))
cancelMutex.withLock {
if (finished && workerScope.isActive) {
workerScope.cancel()
}
}
}
}
}
class ExampleApplication : CoroutineScope {
private val cameraAdapter: CameraAdapter = ...
private val imageProcessor: ImageProcessor<Result> = ...
fun analyzeCameraStream() {
imageProcessor.subscribeTo(cameraAdapter.getImageStream())
}
}
What's the proper way to do this? I would like to use a ChannelFlow instead of a Channel to pass data between the camera and the ImageProcessor. This would allow me to call flow.map to add metadata to the images before they're sent to the analyzers. However, when doing so, each ImageAnalyzer gets a copy of the same image instead of processing different images in parallel. Is it possible to use a Flow as a message queue rather than a broadcaster?
I got this working with flows! It was important to keep the flows backed by a channel throughout this sequence so that each worker would pick up unique images to operate on. I've confirmed this functionality through unit tests.
Here's my updated code for posterity:
class ImageAnalyzer<Result> {
fun analyze(image: Bitmap): Result {
// perform some work on the image and return a Result. This can take a long time.
}
}
class CameraAdapter {
private val imageStream = Channel<Bitmap>(capacity = Channel.RENDEZVOUS)
private val imageReceiveMutex = Mutex()
// additional code to make this camera work and listen to lifecycle events of the enclosing activity.
protected fun sendImageToStream(image: CameraOutput) {
// use channel.offer to enforce the drop back pressure strategy
runBlocking { imageChannel.offer(image) }
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
runBlocking { imageChannel.close() }
}
/**
* Get the stream of images from the camera.
*/
fun getImageStream(): Flow<Bitmap> = imageChannel.receiveAsFlow()
}
class ImageProcessor<Result>(workers: List<ImageAnalyzer<Result>>) {
private val analysisResults = Channel<Result>(capacity = Channel.RENDEZVOUS)
private val cancelMutex = Mutex()
var finished = false // this can be set elsewhere when enough images have been analyzed
fun subscribeTo(flow: Flow<Bitmap>, processingCoroutineScope: CoroutineScope): Job {
// omit some checks to make sure this is not already subscribed
return processingCoroutineScope.launch {
val workerScope = this
workers.forEachIndexed { index, worker ->
launch(Dispatchers.Default) {
startWorker(flow, workerScope, index, worker)
}
}
}
}
private suspend fun startWorker(
flow: Flow<Bitmap>,
workerScope: CoroutineScope,
workerId: Int,
worker: ImageAnalyzer
) {
while (workerScope.isActive) {
flow.collect { bitmap ->
analysisResults.send(worker.analyze(bitmap))
cancelMutex.withLock {
if (finished && workerScope.isActive) {
workerScope.cancel()
}
}
}
}
}
fun getAnalysisResults(): Flow<Result> = analysisResults.receiveAsFlow()
}
class ExampleApplication : CoroutineScope {
private val cameraAdapter: CameraAdapter = ...
private val imageProcessor: ImageProcessor<Result> = ...
fun analyzeCameraStream() {
imageProcessor.subscribeTo(cameraAdapter.getImageStream())
}
}
It appears that, so long as the flow is backed by a channel, the subscribers will each get a unique image.
I'm trying to adapt Google's LocationsUpdatesForegroundService example into Kotlin to use in my app. Now, everything is going fine, until I need to make a reference to a service equal to null. That doesn't 'cause any problems within the Java code it originates from but, when I try to implement it in Kotlin, even if use null!!, I get a KotlinNullPointerException when I try to run the app and the app crashes. I'm not quite sure how to avoid this or set it in a different way. I've spent a few hours on this and sometime browsing StackOverFlow without really being able to find a solution for it. If anyone could help me, it'd be greatly appreciated. I've enclosed the link to the code I'm going off of here:
https://github.com/android/location-samples/blob/master/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java#L127
...as well as the necessary code I'm using below.
Relevant code from my Main Activity:
private var lservice : LocService = null!! // A reference to the service to get location updates
private var bound = false // Tracks the bound state of the service
// Monitors the state of the connection to the service.
private val mServiceConnection = object:ServiceConnection {
override fun onServiceConnected(name:ComponentName, service: IBinder) {
val binder : LocService.LocalBinder = service as LocService.LocalBinder
lservice = binder.getService()
bound = true
}
override fun onServiceDisconnected(name: ComponentName) {
lservice = null!!
bound = false
}
}
My service class, which may or may not be necessary for helping to debug this error:
class LocService : Service() {
private val PACKAGE_NAME = "com.example.localization"
private val TAG = LocService::class.java!!.getSimpleName()
val ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"
private val EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME + ".started_from_notification"
// To return a current instance of the service
private val binder = LocalBinder()
// To check if the bounded activity has actually gone away
// and not unbound as part of an orientation change
private var changingConfig = false
private lateinit var fusedLocClient: FusedLocationProviderClient // For FusedLocationProvider API
private lateinit var locRequest : LocationRequest // Parameters for FusedLocationProvider
// Callback for changes in location
private lateinit var locCallback: LocationCallback
private lateinit var serviceHandler : Handler
private lateinit var notificationManager : NotificationManager // Notification Manager
private lateinit var loc : Location // The current location
// The identifier for the notification displayed for the foreground service
private val NOTIFICATION_ID = 12345678
// Set up when the service is created
override fun onCreate()
{
// An instance of Fused Location Provider Client
fusedLocClient = LocationServices.getFusedLocationProviderClient(this)
// Obtains location callback
locCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
super.onLocationResult(locationResult)
loc = locationResult!!.getLastLocation() // Obtains last location
// Send location information to any broadcast receivers
val intention = Intent(ACTION_BROADCAST)
intention.putExtra("Coordinates", locationResult!!.getLastLocation())
intention.putExtra("Address", getAddress(locationResult))
intention.putExtra("Time", SimpleDateFormat("MM/dd/yyyy 'at' HH:mm").format(Date()))
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intention)
// Change notification content if the service is running in the foreground
if (serviceIsRunningInForeground(this#LocService))
{
notificationManager.notify(NOTIFICATION_ID, getNotification())
}
}
}
// Create location request and get the last location
getLastLocation()
buildLocReq()
// Creates a HandlerThread, which is an extension of Thread and works
// with a Looper, meaning it's meant to handle multiple jobs in the background
// thread. The Looper is what keeps the thread alive. Notification Manager
// is there to notify the user of the notification service
val handlerThread = HandlerThread(TAG)
handlerThread.start()
serviceHandler = Handler(handlerThread.getLooper())
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
// Called whenever the client starts the service using startService()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val startedFromNotification = intent!!.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, false)
return START_NOT_STICKY // Don't recreate the service after it's killed
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
changingConfig = true
}
// Called when the client comes to the foreground and binds
// with this service. The service will stop being a foreground
// service when that happens
override fun onBind(intent: Intent): IBinder {
stopForeground(true)
changingConfig = false
return binder
}
// Called when the client returns to the foreground
// and binds once again with this service. The service will
// stop being a foreground service when that happens
override fun onRebind(intent: Intent?) {
stopForeground(true)
changingConfig = false
super.onRebind(intent)
}
// Called when the client unbinds with the service. If it's called
// with a configuration change, do nothing. Otherwise, make the service
// a foreground service
override fun onUnbind(intent: Intent?): Boolean {
if (!changingConfig && requestingLocationUpdates(this))
{
startForeground(NOTIFICATION_ID, getNotification())
}
return true
}
// Called when service is destroyed
override fun onDestroy() {
serviceHandler.removeCallbacksAndMessages(null)
}
inner class LocalBinder : Binder()
{
fun getService() : LocService
{
return this#LocService
}
}
// For obtaining location request
private fun buildLocReq()
{
// Create a location request to store parameters for the requests
locRequest = LocationRequest.create()
// Sets priority, interval, and --smallest displacement-- for requests
locRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locRequest.interval = 5000
// locRequest.smallestDisplacement = 10f
}
private fun getLastLocation() {
try
{
fusedLocClient.getLastLocation()
.addOnCompleteListener(object:OnCompleteListener<Location>
{
override fun onComplete(#NonNull task:Task<Location>) {
if (task.isSuccessful() && task.getResult() != null)
{
loc = task.getResult() as Location
}
else
{
Log.w(TAG, "Failed to get location.")
}
}
})
}
catch (unlikely:SecurityException) {
Log.e(TAG, "Lost location permission." + unlikely)
}
}
fun requestLocationUpdates()
{
setRequestingLocationUpdates(this, true)
startService(Intent(getApplicationContext(), LocService::class.java))
try
{
fusedLocClient.requestLocationUpdates(locRequest, locCallback, Looper.myLooper())
} catch (unlikely:SecurityException)
{
setRequestingLocationUpdates(this, false)
Log.e(TAG, "Lost location permission. Couldn't request updates. " + unlikely)
}
}
// Obtain address via GeoCoder class
private fun getAddress(locResult: LocationResult?): String {
var address = ""
var geoCoder = Geocoder(this, Locale.getDefault())
var loc1 = locResult!!.locations.get(locResult.locations.size-1)
try {
var addresses:ArrayList<Address> = geoCoder.getFromLocation(loc1.latitude, loc1.longitude, 1) as ArrayList<Address>
address = addresses.get(0).getAddressLine(0)
} catch (e: IOException) {
e.printStackTrace()
}
return address
}
private fun getNotification(): Notification {
val intent = Intent(this, LocService::class.java)
val text = getLocationText(loc)
val builder = NotificationCompat.Builder(this)
.setContentText(text)
.setOngoing(true)
.setPriority(Notification.PRIORITY_HIGH)
.setTicker(text)
.setWhen(System.currentTimeMillis())
return builder.build()
}
// Checks to see if the service is running in the foreground or not
fun serviceIsRunningInForeground(context: Context) : Boolean
{
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Integer.MAX_VALUE))
{
if (javaClass.getName().equals(service.service.getClassName()))
{
if (service.foreground)
{
return true
}
}
}
return false
}
val KEY_REQUESTING_LOCATION_UPDATES = "requesting_locaction_updates"
// Returns true if the requesting location updates, else false
fun requestingLocationUpdates(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(KEY_REQUESTING_LOCATION_UPDATES, false)
}
// Stores the location updates state in SharedPreferences
fun setRequestingLocationUpdates(context: Context, requestingLocationUpdates: Boolean)
{
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(KEY_REQUESTING_LOCATION_UPDATES, requestingLocationUpdates).apply()
}
// Returns the coordinates as a string for the notifications
fun getLocationText(loc: Location) : String
{
if (loc == null) {
return "Unknown Location"
} else {
return "Latitude: " + loc.longitude.toString() + " | Longitude: " + loc.longitude.toString()
}
}
}
Here's the error:
11-01 00:27:36.923 15995-15995/com.example.localization E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.localization, PID: 15995
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.example.localization/com.example.localization.MainActivity}: kotlin.KotlinNullPointerException
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2327)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: kotlin.KotlinNullPointerException
at com.example.localization.MainActivity.<init>(MainActivity.kt:40)
at java.lang.Class.newInstance(Native Method)
at android.app.Instrumentation.newActivity(Instrumentation.java:1067)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2317)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
You declare lservice as:
private var lservice: LocService
That means it's not nullable; Kotlin won't allow you to set it to null.
(In particular, note that null!! will always throw an exception: the !! operator tells the compiler to treat an expression as non-null, or to throw an exception if it is. And since null obviously is null, you're guaranteed an exception!)
If you want to allow the service to be null, you'll have to declare it as:
private var lservice: LocService?
The ? in the type means that it's nullable. As a result, you'll be able to set it to null without any exception. However, you'll need to check whether it's null when you use it, to prevent a NullPointerException there.
Nullability is pretty basic to Kotlin. It's all explained in the Kotlin language docs.
My application (typical REST server that calls other REST services internally) has two main classes to perform the bootstrapping procedure.
There is the Application.kt class that is supposed to configure the vertx instance itself and to register certain modules (jackson kotlin integration for example):
class Application(
private val profileSetting: String? = System.getenv("ACTIVE_PROFILES"),
private val logger: Logger = LoggerFactory.getLogger(Application::class.java)!!
) {
fun bootstrap() {
val profiles = activeProfiles()
val meterRegistry = configureMeters()
val vertx = bootstrapVertx(meterRegistry)
vertx.deployVerticle(ApplicationBootstrapVerticle(profiles)) { startup ->
if (startup.succeeded()) {
logger.info("Application startup finished")
} else {
logger.error("Application startup failed", startup.cause())
vertx.close()
}
}
}
}
In addition there is a ApplicationBootstrapVerticle.kt class that is supposed to deploy the different verticles in a defined order. Some of them in sequence, some of them in parallel:
class ApplicationBootstrapVerticle(
private val profiles: List<String>,
private val logger: Logger = LoggerFactory.getLogger(ApplicationBootstrapVerticle::class.java)
) : AbstractVerticle() {
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}
private fun initializeApplicationConfig(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
ApplicationConfigVerticle(profiles),
it.completer()
)
}
}
private fun initializeScheduledJobs(): CompositeFuture {
val stationsJob = Future.future<String>()
val capabilitiesJob = Future.future<String>()
return CompositeFuture.all(stationsJob, capabilitiesJob).also {
vertx.deployVerticle(
StationQualitiesVerticle(),
stationsJob.completer()
)
vertx.deployVerticle(
VideoCapabilitiesVerticle(),
capabilitiesJob.completer()
)
}
}
private fun initializeRestEndpoints(): Future<String> {
return Future.future<String>().also {
vertx.deployVerticle(
RestEndpointVerticle(dispatcherFactory = RouteDispatcherFactory(vertx)),
it.completer()
)
}
}
}
I am not sure if this is the supposed way to bootstrap an application, if there is any. More important though, I am not sure if I understand the Future.compose mechanics correctly.
The application starts up successfully and I see all desired log messages except the
Application startup finished
message. Also the following code is never called in case of successs:
}.setHandler { ar ->
if (ar.succeeded()) {
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
In case of an failure though, for example when my application configuration files (yaml) cannot be parsed because there is an unknown field in the destination entity, the log message
Application startup failed
appears in the logs and also the code above is invoked.
I am curious what is wrong with my composed futures chain. I thought that the handler would be called after the previous futures succeeded or one of them failed but I think it's only called in case of success.
Update
I suppose that an invocation of startFuture.complete() was missing. By adapting the start method, it finally worked:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.compose {
logger.info("Http server started")
startFuture.complete()
startFuture
}.setHandler(
startFuture.completer()
)
}
I am not sure though, if this is the supposed way to handle this future chain.
The solution that worked for me looks like this:
override fun start(startFuture: Future<Void>) {
initializeApplicationConfig().compose {
logger.info("Application configuration initialized")
initializeScheduledJobs()
}.compose {
logger.info("Scheduled jobs initialized")
initializeRestEndpoints()
}.setHandler { ar ->
if(ar.succeeded()) {
logger.info("Http server started")
startFuture.complete()
} else {
startFuture.fail(ar.cause())
}
}
}