Here is, what I'm trying to do:
A Switch is turned on, starting a service in another thread (works fine so far)
When this service is successful, it should then start another function within the main thread
I don't mind whether the function is called directly by the service or the service is returning a "success"-value to the main thread, what then starts the next function from there.
Here is, what the important parts of the code looks like:
Main thread:
class SendNotif : AppCompatActivity() {
val context = this
private lateinit var Switch: Switch
// Start LocationService when the switch is on
Switch.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
Toast.makeText(context, "Starting LocationService", Toast.LENGTH_SHORT).show()
Intent(applicationContext, LocationService::class.java).apply {
action = LocationService.ACTION_START
startService(this)
}
} else {
Toast.makeText(context, "Stopping LocationService", Toast.LENGTH_SHORT).show()
Intent(applicationContext, LocationService::class.java).apply {
action = LocationService.ACTION_STOP
startService(this)
}
}
}
}
fun InitiateMessage() {
// This is the function, that is supposed to start after the LocationService
}
}
This is the LocationService. After being successful, the function InitiateMessage() should start.
class LocationService: Service() {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var locationClient: LocationClient
var lat = 0.0F
var long = 0.0F
override fun onBind(p0: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
locationClient = DefaultLocationClient(
applicationContext,
LocationServices.getFusedLocationProviderClient(applicationContext)
)
}
// Start or stop the service
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when(intent?.action) {
ACTION_START -> start()
ACTION_STOP -> stop()
}
return super.onStartCommand(intent, flags, startId)
}
private fun start() {
// Starting notification
val notification = NotificationCompat.Builder(this, "location")
.setContentTitle("Tracking location...")
.setContentText("Location: null")
.setSmallIcon(R.drawable.ic_launcher_background)
// Can't swipe this notification away
.setOngoing(true)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Starting the location updates
locationClient
// Every 10 seconds
.getLocationUpdates(10000L)
.catch { e -> e.printStackTrace() }
.onEach { location ->
lat = location.latitude.toString().toFloat() // .takeLast(3) // taking only the last 3 digits
long = location.longitude.toString().toFloat() // .takeLast(3)
val updatedNotification = notification.setContentText(
"Location: ($lat, $long)"
)
// notificationManager.notify(1, updatedNotification.build())
// Geofence
MyGeofence(lat, long)
}
.launchIn(serviceScope)
// startForeground(1, notification.build())
}
private fun stop() {
// Stopping the notification
stopForeground(true)
// Stopping the location service
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
companion object {
const val ACTION_START = "ACTION_START"
const val ACTION_STOP = "ACTION_STOP"
}
fun MyGeofence(lat : Float, long : Float){
val context = this
var db = DataBaseHandler(context)
var data = db.readData()
// Setting the accuracy of the geofence
val acc = 2
val safelat : Double = data.get(0).LocLat.toFloat().round(acc)
val safelong = data.get(0).LocLong.toFloat().round(acc) // .take(acc).take(acc)
val h = Handler(context.mainLooper)
if(safelat == lat.toFloat().round(acc) && safelong == long.toFloat().round(acc)){
h.post(Runnable { Toast.makeText(context, "You have reached your safe refuge! " + lat.toFloat().round(acc) + " " + long.toFloat().round(acc), Toast.LENGTH_LONG).show() })
// ToDo: Right hereafter the function InitiateMessage() should start
}
else{
h.post(Runnable { Toast.makeText(context, "You are still in great danger! " + lat.toFloat().round(acc) + " " + long.toFloat().round(acc), Toast.LENGTH_LONG).show() })
}
}
fun Float.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return round(this * multiplier) / multiplier
}
}
So far, I tried it with a Looper, which did not work.
java.lang.RuntimeException: Can't create handler inside thread Thread[DefaultDispatcher-worker-1,5,main] that has not called Looper.prepare()
But I guess the far easier way would be a returned value by the service. How do I implement this, and how do I start the next function through this returned value?
I solved my problem with an observe-function and a companion object, that is a MutableLiveData.
The companion object is placed inside the main thread:
companion object {
// var iamsafe: Boolean = false
val iamsafe: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
}
The observe-function is placed within onCreate:
val safeObserver = Observer<Boolean> { newState ->
Toast.makeText(context, "Initiating message to my mate.", Toast.LENGTH_SHORT).show()
InitiateMessage()
}
iamsafe.observe(this, safeObserver)
The companion is changed in the second thread like this:
SendNotif.iamsafe.postValue (true)
I have two filters regexFilter and lastModified.
return IntegrationFlows.from(Sftp.inboundAdapter(inboundSftp)
.localDirectory(this.getlocalDirectory(config.getId()))
.deleteRemoteFiles(true)
.autoCreateLocalDirectory(true)
.regexFilter(config.getRegexFilter())
.filter(new LastModifiedLsEntryFileListFilter())
.remoteDirectory(config.getInboundDirectory())
, e -> e.poller(Pollers.fixedDelay(60_000)
.errorChannel(MessageHeaders.ERROR_CHANNEL).errorHandler((ex) -> {
})))
By googling I understand I have to use CompositeFileListFilter for regex so change my code to
.filter(new CompositeFileListFilter().addFilter(new RegexPatternFileListFilter(config.getRegexFilter())))
Its compiled but on run time throws error and channel stooped and same error goes for
.filter(ftpPersistantFilter(config.getRegexFilter()))
.
.
.
public CompositeFileListFilter ftpPersistantFilter(String regexFilter) {
CompositeFileListFilter filters = new CompositeFileListFilter();
filters.addFilter(new FtpRegexPatternFileListFilter(regexFilter));
return filters;
}
I just want to filter on the basis of file name. There are 2 flows for same remote folder and both are polling with same cron but should pick their relevant file.
EDIT
adding last LastModifiedLsEntryFileListFilter. Its working fine but adding upon request.
public class LastModifiedLsEntryFileListFilter implements FileListFilter<LsEntry> {
private final Logger log = LoggerFactory.getLogger(LastModifiedLsEntryFileListFilter.class);
private static final long DEFAULT_AGE = 60;
private volatile long age = DEFAULT_AGE;
private volatile Map<String, Long> sizeMap = new HashMap<String, Long>();
public long getAge() {
return this.age;
}
public void setAge(long age) {
setAge(age, TimeUnit.SECONDS);
}
public void setAge(long age, TimeUnit unit) {
this.age = unit.toSeconds(age);
}
#Override
public List<LsEntry> filterFiles(LsEntry[] files) {
List<LsEntry> list = new ArrayList<LsEntry>();
long now = System.currentTimeMillis() / 1000;
for (LsEntry file : files) {
if (file.getAttrs()
.isDir()) {
continue;
}
String fileName = file.getFilename();
Long currentSize = file.getAttrs().getSize();
Long oldSize = sizeMap.get(fileName);
if(oldSize == null || currentSize.longValue() != oldSize.longValue() ) {
// putting size in map, will verify in next iteration of scheduler
sizeMap.put(fileName, currentSize);
log.info("[{}] old size [{}] increased to [{}]...", file.getFilename(), oldSize, currentSize);
continue;
}
int lastModifiedTime = file.getAttrs()
.getMTime();
if (lastModifiedTime + this.age <= now ) {
list.add(file);
sizeMap.remove(fileName);
} else {
log.info("File [{}] is still being uploaded...", file.getFilename());
}
}
return list;
}
}
PS : When I am testing filter for regex I have removed LastModifiedLsEntryFileListFilter just for simplicity. So my final Flow is
return IntegrationFlows.from(Sftp.inboundAdapter(inboundSftp)
.localDirectory(this.getlocalDirectory(config.getId()))
.deleteRemoteFiles(true)
.autoCreateLocalDirectory(true)
.filter(new CompositeFileListFilter().addFilter(new RegexPatternFileListFilter(config.getRegexFilter())))
//.filter(new LastModifiedLsEntryFileListFilter())
.remoteDirectory(config.getInboundDirectory()),
e -> e.poller(Pollers.fixedDelay(60_000)
.errorChannel(MessageHeaders.ERROR_CHANNEL).errorHandler((ex) -> {
try {
this.destroy(String.valueOf(config.getId()));
configurationService.removeConfigurationChannelById(config.getId());
// // logging here
} catch (Exception ex1) {
}
}))).publishSubscribeChannel(s -> s
.subscribe(f -> {
f.handle(Sftp.outboundAdapter(outboundSftp)
.useTemporaryFileName(false)
.autoCreateDirectory(true)
.remoteDirectory(config.getOutboundDirectory()), c -> c.advice(startup.deleteFileAdvice()));
})
.subscribe(f -> {
if (doArchive) {
f.handle(Sftp.outboundAdapter(inboundSftp)
.useTemporaryFileName(false)
.autoCreateDirectory(true)
.remoteDirectory(config.getInboundArchiveDirectory()));
} else {
f.handle(m -> {
});
}
})
.subscribe(f -> f
.handle(m -> {
// I am handling exception here
})
))
.get();
and here are exceptions
2020-01-27 21:36:55,731 INFO o.s.i.c.PublishSubscribeChannel - Channel
'application.2.subFlow#0.channel#0' has 0 subscriber(s).
2020-01-27 21:36:55,731 INFO o.s.i.e.EventDrivenConsumer - stopped 2.subFlow#2.org.springframework.integration.config.ConsumerEndpointFactoryBean#0
2020-01-27 21:36:55,731 INFO o.s.i.c.DirectChannel - Channel 'application.2.subFlow#2.channel#0' has 0 subscriber(s).
2020-01-27 21:36:55,731 INFO o.s.i.e.EventDrivenConsumer - stopped 2.subFlow#2.org.springframework.integration.config.ConsumerEndpointFactoryBean#1
EDIT
After passing regex to LastModifiedLsEntryFileListFilter and handle there works for me. When I use any other RegexFilter inside CompositeFileListFilter it thorws error.
.filter(new CompositeFileListFilter().addFilter(new LastModifiedLsEntryFileListFilter(config.getRegexFilter())))
Show, please, your final flow. I don't see that you use LastModifiedLsEntryFileListFilter in your CompositeFileListFilter... You definitely can't use regexFilter() and filter() together - the last one wins. To avoid confusion we suggest to use a filter() and compose all those with CompositeFileListFilter or ChainFileListFilter.
Also what is an error you are mentioning, please.
Spring's reactor has an interesting feature : Hedging . It means spawning many requests and get the first returned result , and automatically clean other contexts. Josh Long recently has been actively promoting this feature. Googling Spring reactor hedging shows relative results. If anybody is curious , here is the sample code . In short , Flux.first() simplifies all the underlaying hassles , which is very impressive.
I wonder how this can be achieved with Kotlin's coroutine and multithread , (and maybe with Flow or Channel ) . I thought of a simple scenario : One service accepts longUrl and spawns the longUrl to many URL shorten service ( such as IsGd , TinyUrl ...) , and returns the first returned URL ... (and terminates / cleans other thread / coroutine resources)
There is an interface UrlShorter that defines this work :
interface UrlShorter {
fun getShortUrl(longUrl: String): String?
}
And there are three implementations , one for is.gd , another for tinyUrl , and the third is a Dumb implementation that blocks 10 seconds and return null :
class IsgdImpl : UrlShorter {
override fun getShortUrl(longUrl: String): String? {
logger.info("running : {}", Thread.currentThread().name)
// isGd api url blocked by SO , it sucks . see the underlaying gist for full code
val url = "https://is.gd/_create.php?format=simple&url=%s".format(URLEncoder.encode(longUrl, "UTF-8"))
return Request.Get(url).execute().returnContent().asString().also {
logger.info("returning {}", it)
}
}
}
class TinyImpl : UrlShorter {
override fun getShortUrl(longUrl: String): String? {
logger.info("running : {}", Thread.currentThread().name)
val url = "http://tinyurl.com/_api-create.php?url=$longUrl" // sorry the URL is blocked by stackoverflow , see the underlaying gist for full code
return Request.Get(url).execute().returnContent().asString().also {
logger.info("returning {}", it)
}
}
}
class DumbImpl : UrlShorter {
override fun getShortUrl(longUrl: String): String? {
logger.info("running : {}", Thread.currentThread().name)
TimeUnit.SECONDS.sleep(10)
return null
}
}
And there is a UrlShorterService that takes all the UrlShorter implementations , and try to spawn coroutines and get the first result .
Here is what I've thought of :
#ExperimentalCoroutinesApi
#FlowPreview
class UrlShorterService(private val impls: List<UrlShorter>) {
private val es: ExecutorService = Executors.newFixedThreadPool(impls.size)
private val esDispatcher = es.asCoroutineDispatcher()
suspend fun getShortUrl(longUrl: String): String {
return method1(longUrl) // there are other methods , with different ways...
}
private inline fun <T, R : Any> Iterable<T>.firstNotNullResult(transform: (T) -> R?): R? {
for (element in this) {
val result = transform(element)
if (result != null) return result
}
return null
}
The client side is simple too :
#ExperimentalCoroutinesApi
#FlowPreview
class UrlShorterServiceTest {
#Test
fun testHedging() {
val impls = listOf(DumbImpl(), IsgdImpl(), TinyImpl()) // Dumb first
val service = UrlShorterService(impls)
runBlocking {
service.getShortUrl("https://www.google.com").also {
logger.info("result = {}", it)
}
}
}
}
Notice I put the DumbImpl first , because I hope it may spawn first and blocking in its thread. And other two implementations can get result.
OK , here is the problem , how to achieve hedging in kotlin ? I try the following methods :
private suspend fun method1(longUrl: String): String {
return impls.asSequence().asFlow().flatMapMerge(impls.size) { impl ->
flow {
impl.getShortUrl(longUrl)?.also {
emit(it)
}
}.flowOn(esDispatcher)
}.first()
.also { esDispatcher.cancelChildren() } // doesn't impact the result
}
I hope method1 should work , but it totally executes 10 seconds :
00:56:09,253 INFO TinyImpl - running : pool-1-thread-3
00:56:09,254 INFO DumbImpl - running : pool-1-thread-1
00:56:09,253 INFO IsgdImpl - running : pool-1-thread-2
00:56:11,150 INFO TinyImpl - returning // tiny url blocked by SO , it sucks
00:56:13,604 INFO IsgdImpl - returning // idGd url blocked by SO , it sucks
00:56:19,261 INFO UrlShorterServiceTest$testHedging$1 - result = // tiny url blocked by SO , it sucks
Then , I thought other method2 , method3 , method4 , method5 ... but all not work :
/**
* 00:54:29,035 INFO IsgdImpl - running : pool-1-thread-3
* 00:54:29,036 INFO DumbImpl - running : pool-1-thread-2
* 00:54:29,035 INFO TinyImpl - running : pool-1-thread-1
* 00:54:30,228 INFO TinyImpl - returning // tiny url blocked by SO , it sucks
* 00:54:30,797 INFO IsgdImpl - returning // idGd url blocked by SO , it sucks
* 00:54:39,046 INFO UrlShorterServiceTest$testHedging$1 - result = // idGd url blocked by SO , it sucks
*/
private suspend fun method2(longUrl: String): String {
return withContext(esDispatcher) {
impls.map { impl ->
async(esDispatcher) {
impl.getShortUrl(longUrl)
}
}.firstNotNullResult { it.await() } ?: longUrl
}
}
/**
* 00:52:30,681 INFO IsgdImpl - running : pool-1-thread-2
* 00:52:30,682 INFO DumbImpl - running : pool-1-thread-1
* 00:52:30,681 INFO TinyImpl - running : pool-1-thread-3
* 00:52:31,838 INFO TinyImpl - returning // tiny url blocked by SO , it sucks
* 00:52:33,721 INFO IsgdImpl - returning // idGd url blocked by SO , it sucks
* 00:52:40,691 INFO UrlShorterServiceTest$testHedging$1 - result = // idGd url blocked by SO , it sucks
*/
private suspend fun method3(longUrl: String): String {
return coroutineScope {
impls.map { impl ->
async(esDispatcher) {
impl.getShortUrl(longUrl)
}
}.firstNotNullResult { it.await() } ?: longUrl
}
}
/**
* 01:58:56,930 INFO TinyImpl - running : pool-1-thread-1
* 01:58:56,933 INFO DumbImpl - running : pool-1-thread-2
* 01:58:56,930 INFO IsgdImpl - running : pool-1-thread-3
* 01:58:58,411 INFO TinyImpl - returning // tiny url blocked by SO , it sucks
* 01:58:59,026 INFO IsgdImpl - returning // idGd url blocked by SO , it sucks
* 01:59:06,942 INFO UrlShorterServiceTest$testHedging$1 - result = // idGd url blocked by SO , it sucks
*/
private suspend fun method4(longUrl: String): String {
return withContext(esDispatcher) {
impls.map { impl ->
async {
impl.getShortUrl(longUrl)
}
}.firstNotNullResult { it.await() } ?: longUrl
}
}
I am not familiar with Channel , sorry for the exception ↓
/**
* 01:29:44,460 INFO UrlShorterService$method5$2 - channel closed
* 01:29:44,461 INFO DumbImpl - running : pool-1-thread-2
* 01:29:44,460 INFO IsgdImpl - running : pool-1-thread-3
* 01:29:44,466 INFO TinyImpl - running : pool-1-thread-1
* 01:29:45,765 INFO TinyImpl - returning // tiny url blocked by SO , it sucks
* 01:29:46,339 INFO IsgdImpl - returning // idGd url blocked by SO , it sucks
*
* kotlinx.coroutines.channels.ClosedSendChannelException: Channel was closed
*
*/
private suspend fun method5(longUrl: String): String {
val channel = Channel<String>()
withContext(esDispatcher) {
impls.forEach { impl ->
launch {
impl.getShortUrl(longUrl)?.also {
channel.send(it)
}
}
}
channel.close()
logger.info("channel closed")
}
return channel.consumeAsFlow().first()
}
OK , I don't know if there are any other ways ... but all above are not working... All blocks at least 10 seconds ( blocked by DumbImpl) .
The whole source code can be found on github gist .
How can hedging be achieved in kotlin ? By Deferred or Flow or Channel or any other better ideas ? Thank you.
After submitting the question , I found all tinyurl , isGd url are blocked by SO . It really sucks !
If the actual work you want to do in parallel consists of network fetches, you should choose an async networking library so you can properly use non-blocking coroutines with it. For example, as of version 11 the JDK provides an async HTTP client which you can use as follows:
val httpClient: HttpClient = HttpClient.newHttpClient()
suspend fun httpGet(url: String): String = httpClient
.sendAsync(
HttpRequest.newBuilder().uri(URI.create(url)).build(),
BodyHandlers.ofString())
.await()
.body()
Here's a function that accomplishes request hedging given a suspendable implementation like above:
class UrlShortenerService(
private val impls: List<UrlShortener>
) {
suspend fun getShortUrl(longUrl: String): String? = impls
.asFlow()
.flatMapMerge(impls.size) { impl ->
flow<String?> {
try {
impl.getShortUrl(longUrl)?.also { emit(it) }
}
catch (e: Exception) {
// maybe log it, but don't let it propagate
}
}
}
.onCompletion { emit(null) }
.first()
}
Note the absence of any custom dispatchers, you don't need them for suspendable work. Any dispatcher will do, and all the work can run in a single thread.
The onCompletion parts steps into action when your all URL shorteners fail. In that case the flatMapMerge stage doesn't emit anything and first() would deadlock without the extra null injected into the flow.
To test it I used the following code:
class Shortener(
private val delay: Long
) : UrlShortener {
override suspend fun getShortUrl(longUrl: String): String? {
delay(delay * 1000)
println("Shortener $delay completing")
if (delay == 1L) {
throw Exception("failed service")
}
if (delay == 2L) {
return null
}
return "shortened after $delay seconds"
}
}
suspend fun main() {
val shorteners = listOf(
Shortener(4),
Shortener(3),
Shortener(2),
Shortener(1)
)
measureTimeMillis {
UrlShortenerService(shorteners).getShortUrl("bla").also {
println(it)
}
}.also {
println("Took $it ms")
}
}
This exercises the various failure cases like returning null or failing with an exception. For this code I get the following output:
Shortener 1 completing
Shortener 2 completing
Shortener 3 completing
shortened after 3 seconds
Took 3080 ms
We can see that the shorteners 1 and 2 completed but with a failure, shortener 3 returned a valid response, and shortener 4 was cancelled before completing. I think this matches the requirements.
If you can't move away from blocking requests, your implementation will have to start num_impls * num_concurrent_requests threads, which is not great. However, if that's the best you can have, here's an implementation that hedges blocking requests but awaits on them suspendably and cancellably. It will send an interrupt signal to the worker threads running the requests, but if your library's IO code is non-interruptible, these threads will hang waiting for their requests to complete or time out.
val es = Executors.newCachedThreadPool()
interface UrlShortener {
fun getShortUrl(longUrl: String): String? // not suspendable!
}
class UrlShortenerService(
private val impls: List<UrlShortener>
) {
suspend fun getShortUrl(longUrl: String): String {
val chan = Channel<String?>()
val futures = impls.map { impl -> es.submit {
try {
impl.getShortUrl(longUrl)
} catch (e: Exception) {
null
}.also { runBlocking { chan.send(it) } }
} }
try {
(1..impls.size).forEach { _ ->
chan.receive()?.also { return it }
}
throw Exception("All services failed")
} finally {
chan.close()
futures.forEach { it.cancel(true) }
}
}
}
This is essentially what the select APi was designed to do:
coroutineScope {
select {
impls.forEach { impl ->
async {
impl.getShortUrl(longUrl)
}.onAwait { it }
}
}
coroutineContext[Job].cancelChildren() // Cancel any requests that are still going.
}
Note that this will not handle exceptions thrown by the service implementations, you will need to use a supervisorScope with a custom exception handler and a filtering select loop if you want to actually handle those.
I want to test the following class:
public class DBSync {
public dbNotify( String path ) {
if (!path) {
return
}
def pathIndex = path.lastIndexOf(File.separator)
if (pathIndex > 0) {
def folder = path[0..pathIndex - 1]
def fileName = path[pathIndex + 1..path.length() - 1]
println "Syncing from directory $folder for filename $fileName"
if (fileName.contains(EXCLUDE_FILE_PATTERN)) {
println "Filename is $EXCLUDE_FILE_PATTERN skipping db write "
return
}
writeToDB(folder, fileName)
}
}
public writeToDB ( folder, file ) {
// inserting to database
}
}
The test class is:
public class TestDBSync {
#Test
public void test() {
def dbSyncContext = new MockFor(DBSync)
def file = "file.zip"
def folder = "data"
def path = folder + File.separator + file
def called = false
// Expect at least one call
mockDBSyncContext.demand.writeToDB(1..1) { String folderargs, String fileargs -> called = true }
mockDBSyncContext.demand.dbNodify(1..1) { String pathargs -> return }
// Obtaining a usuable mock instance
def mockDBSyncProxy = mockDBSyncContext.proxyInstance()
// Fake calling the method
mockDBSyncContext.use {
mockDBSyncProxy.dbNotify(path )
}
// Verify invoked at least once?
mockDBSyncContext.verify(mockDBSyncProxy)
}
}
The test is failing and I am getting the following error:
junit.framework.AssertionFailedError: No call to 'dbNotify' expected
at this point. Still 1 call(s) to 'writeToDB' expected.
I am trying to read current location coordinates in WatchKitExtension ExtensionDelegate. This does though not return any value.
The very same code used in WatchKitExtension InterfaceController does return the location. (tried this out of desperation as I could not find an error in the code)
I would need to perform this code in ExtensionDelegate as I would like to pass the retrieved location on to a ClockKit Complication.
Here the code in ExtensionDelegate: (after self.locationManager.requestLocation() the delegate functions didUpdateLocation / didFailWithError do not get called)
import WatchKit
import CoreLocation
class ExtensionDelegate: NSObject, WKExtensionDelegate, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
override init() {
print("ExtensionDelegate: \(NSDate()) - init")
super.init()
self.getLocation()
}
func getLocation(){
print("ExtensionDelegate: \(NSDate()) - getLocation")
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
}
...
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("ExtensionDelegate: \(NSDate()) - locationManager didUpdateLocations")
guard let mostRecentLocation = locations.last else { return }
let place = mostRecentLocation.coordinate
print("\(place)")
manager.stopUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("ExtensionDelegate: \(NSDate()) - locationManager didFailWithError")
print("CL failed: \(error)")
}
}
Here the very same code in InterfaceController and it works perfectly (didUpdateLocation does get called):
import WatchKit
import Foundation
import CoreLocation
class InterfaceController: WKInterfaceController, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
override func awakeWithContext(context: AnyObject?) {
print("InterfaceController: \(NSDate()) - awakeWithContext")
super.awakeWithContext(context)
self.getLocation()
}
func getLocation(){
print("InterfaceController: \(NSDate()) - getLocation")
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
}
...
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("InterfaceController: \(NSDate()) - locationManager didUpdateLocations")
guard let mostRecentLocation = locations.last else { return }
let place = mostRecentLocation.coordinate
print("\(place)")
manager.stopUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("InterfaceController: \(NSDate()) - locationManager didFailWithError")
print("CL failed: \(error)")
}
}