14

So I've spent the past few weeks working on my Android App and looking into the best way of implementing what I need to do, but still can't quite get it right.. Any/all help is greatly appreciated, as I am still getting the hang of things..

Task:

(Assuming the "Location"/GPS Setting is currently off), I need to have my App constantly be listening for the "Location" Setting to be turned ON.. At this point, simply start an Activity.

Ideas:

These are all the different ways I think it could possibly work:

  • LocationListener using "onProviderEnabled"

  • GpsStatusListener using "onGpsStatusChanged" with "GPS_EVENT_STARTED"

  • GpsProvider requiresSatellite (to determine if it starts), or somehow use the GpsProvider's "AVAILABLE" Constant/int

  • SettingInjectorService using "ACTION_SERVICE_INTENT" (and/or) "ACTION_INJECTED_SETTING_CHANGED" with "onGetEnabled" or "isEnabled"

  • Settings.Secure using "LOCATION_MODE" != "LOCATION_MODE_OFF"

  • a ContentObserver/ContentResolver

  • Intent getAction (...)

  • An "if/else" of some kind

Questions:

Any kind of advice or answers for any of the following questions are very much appreciated..

  • Which of the Ideas above would be the best way of accomplishing the Task? The simpler the better, but most importantly it needs to be listening at all times, and respond instantly when the Location Setting is turned On.

  • For whichever one of the Ideas above works best, how would I implement it? (For example, would I need a BroadcastListener? or a Service? and how would it all piece together?

I truly appreciate any advice or help that you can provide me with.. I'm still getting the hang of all this, but confident enough to do it, and eager to publish my first App.. So thank you, it means a lot and will greatly help me along.


EDIT:

OK So here's what I've got so far...
Heres my Receiver:

MyReceiver.Java

public class MyReceiver extends BroadcastReceiver {

    private final static String TAG = "LocationProviderChanged";

    boolean isGpsEnabled;
    boolean isNetworkEnabled;



    public MyReceiver() {
    // EMPTY

    // MyReceiver Close Bracket
    }



    // START OF onReceive
    @Override
    public void onReceive(Context context, Intent intent) {


        // PRIMARY RECEIVER
        if (intent.getAction().matches("android.location.PROVIDERS_CHANGED")) {

            Log.i(TAG, "Location Providers Changed");

            LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
            isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
            isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

            Toast.makeText(context, "GPS Enabled: " + isGpsEnabled + " Network Location Enabled: " + isNetworkEnabled, Toast.LENGTH_LONG).show();

            // START DIALOG ACTIVITY
            if (isGpsEnabled || isNetworkEnabled) {
                Intent runDialogActivity = new Intent(context, DialogActivity.class);
                context.startActivity(runDialogActivity);

            }

        }



        // BOOT COMPLETED (REPLICA OF PRIMARY RECEIVER CODE FOR WHEN BOOT_COMPLETED)
        if (intent.getAction().matches("android.intent.action.BOOT_COMPLETED")) {

            Log.i(TAG, "Location Providers Changed Boot");

            LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
            isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
            isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

            Toast.makeText(context, "GPS Enabled Boot: " + isGpsEnabled + " Network Location Enabled Boot: " + isNetworkEnabled, Toast.LENGTH_LONG).show();

            // START DIALOG ACTIVITY
            if (isGpsEnabled || isNetworkEnabled) {
                Intent runDialogActivity = new Intent(context, DialogActivity.class);
                context.startActivity(runDialogActivity);

            }

        }



    // onReceive CLOSE BRACKET
    }



// CLOSE OF FULL CLASS
}

And heres what the Manifest looks like:

Manifest.XML

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ender.projects.receivertest">

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:allowBackup="true"
        android:fullBackupContent="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".DialogActivity">
        </activity>

        <receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.location.PROVIDERS_CHANGED" />

                <action android:name="android.intent.action.BOOT_COMPLETED" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>

    </application>

</manifest>

Aside from these files, I have the following files:
MainActivity.Java and DialogActivity.Java, both with Layout files,
activity_main.xml and activity_dialog.xml to match with them.

So if I understand correctly, when the user downloads the App and opens it, my MainActivity.Java and corresponding layout will launch. But I am only going to use this simply as a preferences screen. So once they open the App for the first time, the Broadcast Receiver should automatically start listening for the LOCATION setting to be turned on, correct? I also want the Broadcast Listener to remain listening, even after it receives the initial Broadcast (so that my onReceive still fires if they turn the LOCATION setting off, and then later they turn it ON again..

So (A) How does my code look thus far?
and (B) To accomplish what I just described, what would need to be added?
and (C) Running it throws this error when I turn the LOCATION setting ON.
..How can I fix it?:
java.lang.RuntimeException: Unable to start receiver com.bryce.projects.servicesthreadsetc.MyReceiver: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?


Thanks again for all the help!

Ender87
  • 181
  • 1
  • 2
  • 8
  • So how exactly would I implement that? It sounded like the GpsStatusListener was a better option since it only needs 1 method, while the LocationListener has like 4.. But in any case, what would the steps be for where to put everything? Since it's already a Listener on its own (I think), should I simply run it within a Service (rather than in a BroadcastListener) or what? – Ender87 Mar 31 '16 at 09:57
  • @DanielNugent So a couple questions.. If I use the LocationListener in order to determine the exact moment the user switches the Location Setting to "ON" (and thus only using onProviderEnabled), would I still need to get the latitude and longitude, etc like in the example? And question 2 is, do I need to use a Handler like in the example as well? If I'm not mistaken, it's hard-coded into the LocationListener already right? As is the function of a BroadcastListener, meaning I don't need that either, and can simply run the code within a Service (or Activity with no Layout) correct? or no? Thanks – Ender87 Apr 01 '16 at 08:02
  • @DanielNugent Again, I don't even need the actual location whatsoever.. I literally ONLY need my App to essentially listen for when "Location" gets turned on, and then start a new activity. That's it. But other than that, my App doesn't have ANYTHING to do with using the Location or anything whatsoever (aside from running in the background and firing up my next activity as soon as the Location SETTING gets turned on).. Does that change anything, as far as the best option? – Ender87 Apr 01 '16 at 08:54
  • @DanielNugent When you say "use the BroadcastReceiver for android. location.PROVIDERS_CHANGED", are you still referring to using the LocationListener (within a Service), OR are you saying to implement some kind of " If/Else" statement containing PROVIDERS_CHANGED somehow, from within an actual BroadcastListener? – Ender87 Apr 02 '16 at 02:17
  • @DanielNugent I wish there was a way for us to communicate more easily.. I have a few more questions about it that are pretty basic, but I don't want to make this post/thread too long.. Would you mind if I emailed you? If I could get your email that would be super helpful in achieving my goals. I really appreciate your help. – Ender87 Apr 02 '16 at 02:23
  • @DanielNugent I took a look at the link you gave me, and it seems to make sense.. However, you mention some kind of "Library" on a few occasions in that post.. Do I need to utilize some kind of Library as well? Or can I simply put that code into a receiver, and have a button in my main app start the receiver up and close itself? – Ender87 Apr 03 '16 at 22:22
  • @DanielNugent Awesome I'll try it out and get back to you with any further questions.. However just curious, whats the difference between running a BroadcastReceiver using an Intent, vs running a Service using the LocationListener? Isnt it basically the same? Remember I need it to be running at all times, even when my App isn't. And also start listening automatically when the system starts etc. So how would the 2 methods differ? – Ender87 Apr 03 '16 at 22:36
  • @DanielNugent For example, in my MainActivity, I have to use an Intent to actually Start my Service when they press a Button.. But do I need to do this for my Receiver in a similar way? Or how does the Receiver get started? – Ender87 Apr 04 '16 at 00:32
  • I just added an answer, I really think it's as simple as that. Let me know if it works for you! – Daniel Nugent Apr 04 '16 at 02:10
  • @DanielNugent I now have the entire Receiver coded, as well as the Manifest, both as you described.. My only remaining questions are: How to make sure the Receiver stays active, in the event that the user turns off the Location Service and then turns it back ON again.. Essentially I want it to be listening at ALL times for it to be turned ON, even after the initial receipt of the Broadcast.. Make sense? Just want to make sure the Receiver doesnt get killed.. And thank you again for all the help.. – Ender87 Apr 04 '16 at 02:23
  • You should read the documentation on BroadcastReceivers, the beauty of them is that they are not always running (so your app is not eating up battery life), but when the provider change happens, the OS starts your app and executes the BroadcastReceiver's onReceive. Go ahead and test it, and make sure that it works for your needs! – Daniel Nugent Apr 04 '16 at 02:32
  • @DanielNugent Awesome, I will try it out, and thanks again.. I actually have the documentation up as we speak.. The only issue is, I want the "Launch my next Activity upon receipt of the Broadcast" to be an Option the user can set to On or Off. But it sounds like the Receiver will always be running. I also need to make sure it doesn't get killed after it receives the initial Broadcast, so it will continue to respond to it. Is there a code needed to reinitialize the Receiver after the onReceive code, or does it automatically reinstate it? – Ender87 Apr 04 '16 at 02:36
  • Just use a SharedPreference value to store what the user has chosen, and check the value of the SharedPreference in the BroadcastReceiver. If the user has disabled it, don't start the Activity in onReceive() in the BroadcastReceiver. – Daniel Nugent Apr 04 '16 at 02:41
  • @DanielNugent OK So I just edited my original post and added all the Code I've got so far.. Could you take a look at my Edit above, and the questions under it? Thanks Daniel! – Ender87 Apr 05 '16 at 00:03

4 Answers4

28

Since you don't need to actually get a Location, the best implementation for your needs would be a BroadcastReceiver.

This is the best option because you wouldn't need to have a Service running at all times (resulting in extra batter drain), and you would be able to start your Activity from the BroadcastReceiver.

With the intent filter and BroadcastReceiver, your app will be started by the OS whenever the Location setting has changed (enabled or disabled), and in the case that it is enabled, you can start your Activity from the BroadcastReceiver.

First add the intent filter, which will be captured when the OS sends out the implicit Intent that the setting has changed.

<receiver
    android:name=".LocationProviderChangedReceiver"
    android:exported="false" >
    <intent-filter>
        <action android:name="android.location.PROVIDERS_CHANGED" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</receiver>

Note that for this to work on Android Oreo and above, you'll need to register the broadcast receiver at runtime, see here: https://developer.android.com/guide/components/broadcasts#context-registered-receivers

Then, in LocationProviderChangedReceiver.java, your implementation would be something like this:

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.location.LocationManager;
import android.util.Log;
import android.widget.Toast;

public class LocationProviderChangedReceiver extends BroadcastReceiver {
    private final static String TAG = "LocationProviderChanged";

    boolean isGpsEnabled;
    boolean isNetworkEnabled;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().matches("android.location.PROVIDERS_CHANGED"))
        {
            Log.i(TAG, "Location Providers changed");

            LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
            isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
            isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

            //Start your Activity if location was enabled:
            if (isGpsEnabled || isNetworkEnabled) {
                  Intent i = new Intent(context, YourActivity.class);
                  context.startActivity(i);
            }
        }
    }
}

Edit

Updated solution with Kotlin and registering receiver at runtime for Android 9.

The BroadcastReceiver class in Kotlin:

class LocationProviderChangedReceiver : BroadcastReceiver() {

    internal var isGpsEnabled: Boolean = false
    internal var isNetworkEnabled: Boolean = false

    override fun onReceive(context: Context, intent: Intent) {
        intent.action?.let { act ->
            if (act.matches("android.location.PROVIDERS_CHANGED".toRegex())) {
                val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
                isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
                isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)

                Log.i(TAG, "Location Providers changed, is GPS Enabled: " + isGpsEnabled)

                //Start your Activity if location was enabled:
                if (isGpsEnabled || isNetworkEnabled) {
                    val i = Intent(context, YourActivity::class.java)
                    context.startActivity(i)
                }
            }
        }
    }

    companion object {
        private val TAG = "LocationProviderChanged"
    }
}

Register at runtime, for example in onCreate() of your app's MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val br: BroadcastReceiver = LocationProviderChangedReceiver()
    val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
    registerReceiver(br, filter)
}
Daniel Nugent
  • 43,104
  • 15
  • 109
  • 137
  • 2
    Hey, what about Android Oreo? we can no longer use the manifest as it violates some new restrictions. It's basically isn't called when app is in background. I understand we should do something with a JobScheduler. Any thoughts? – guy_m May 22 '18 at 15:58
  • 3
    You'll have to use context based registration or dynamic registration. doc [link] (https://developer.android.com/guide/components/broadcasts#context-registered-receivers) – gfache May 22 '18 at 16:53
  • Please just use .equals() instead of .matches(). It also causes you to needlessly call .toRegex() in your Kotlin code – Kelvin Bouma Aug 05 '23 at 14:52
4

Here is a solution using dynamic registration:

In your fragment or activity's onResume() method, listen to changes in LocationManager.PROVIDERS_CHANGED_ACTION

IntentFilter filter = new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION);
filter.addAction(Intent.ACTION_PROVIDER_CHANGED);
mActivity.registerReceiver(gpsSwitchStateReceiver, filter);

Here is a code sample for the gpsSwitchStateReceiver object:

private BroadcastReceiver gpsSwitchStateReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {


            if (LocationManager.PROVIDERS_CHANGED_ACTION.equals(intent.getAction())) {
                // Make an action or refresh an already managed state.

                LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
                boolean isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
                boolean isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

                if (isGpsEnabled || isNetworkEnabled) {
                    Log.i(this.getClass().getName(), "gpsSwitchStateReceiver.onReceive() location is enabled : isGpsEnabled = " + isGpsEnabled + " isNetworkEnabled = " + isNetworkEnabled);
                } else {
                    Log.w(this.getClass().getName(), "gpsSwitchStateReceiver.onReceive() location disabled ");
                }
            }
        }
    };

In your onPause() method, unregister the receiver

mActivity.unregisterReceiver(gpsSwitchStateReceiver);
matdev
  • 4,115
  • 6
  • 35
  • 56
1

Here is an extension property that produces a flow from a Broadcast receiver:

val Context.locationEnabledFlow
    get() = callbackFlow {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                trySend(LocationManagerCompat.isLocationEnabled(locationManager))
            }
        }

        registerReceiver(receiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
        awaitClose { unregisterReceiver(receiver) }
    }

For example, this is how you could use it with Jetpack Compose:

val isLocationEnabled by LocalContext.current.locationEnabledFlow
    .collectAsStateWithLifecycle(initialValue = false)

Text(text = when(isLocationEnabled){
    true -> "Location enabled."
    false -> "Location disabled."
})
argenkiwi
  • 2,134
  • 1
  • 23
  • 26
0

LocationListeners's onProvideEnabled() method will work instantly. When you call this remember if you are doing LocationManager.removeUpdates() in between onProviderEnabled() won't work.

And this is pretty much simple when you are reading locations from Location API's of Android. You don't need the broadcast receivers listening and communication further

But when you are using FusedLocationProviderClient from google API and if you use LocationListener's onProviderEnabled and onProviderDisabled methods you will be having two services running in parallel, checking for the provider changes. So using BroadcastReceiver is better

Mohanakrrishna
  • 148
  • 3
  • 13