// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.moodle.moodlemobile; /* * Imports */ import org.apache.cordova.CordovaWebView; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaInterface; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import android.content.BroadcastReceiver; import android.content.IntentFilter; import android.location.LocationManager; import android.os.Build; import android.util.Log; import android.content.Context; import android.content.Intent; import android.provider.Settings; /** * Diagnostic plugin implementation for Android */ public class Diagnostic_Location extends CordovaPlugin{ /************* * Constants * *************/ /** * Tag for debug log messages */ public static final String TAG = "Diagnostic_Location"; private static String gpsLocationPermission = "ACCESS_FINE_LOCATION"; private static String networkLocationPermission = "ACCESS_COARSE_LOCATION"; private static String backgroundLocationPermission = "ACCESS_BACKGROUND_LOCATION"; private static final String LOCATION_MODE_HIGH_ACCURACY = "high_accuracy"; private static final String LOCATION_MODE_DEVICE_ONLY = "device_only"; private static final String LOCATION_MODE_BATTERY_SAVING = "battery_saving"; private static final String LOCATION_MODE_OFF = "location_off"; private static final String LOCATION_MODE_UNKNOWN = "unknown"; /************* * Variables * *************/ /** * Singleton class instance */ public static Diagnostic_Location instance = null; private Diagnostic diagnostic; public static LocationManager locationManager; /** * Current Cordova callback context (on this thread) */ protected CallbackContext currentContext; private String currentLocationMode = null; /************* * Public API ************/ /** * Constructor. */ public Diagnostic_Location() {} /** * Sets the context of the Command. This can then be used to do things like * get file paths associated with the Activity. * * @param cordova The context of the main Activity. * @param webView The CordovaWebView Cordova is running in. */ public void initialize(CordovaInterface cordova, CordovaWebView webView) { Log.d(TAG, "initialize()"); instance = this; diagnostic = Diagnostic.getInstance(); try { diagnostic.applicationContext.registerReceiver(locationProviderChangedReceiver, new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)); locationManager = (LocationManager) this.cordova.getActivity().getSystemService(Context.LOCATION_SERVICE); }catch(Exception e){ diagnostic.logWarning("Unable to register Location Provider Change receiver: " + e.getMessage()); } try { currentLocationMode = getLocationModeName(); }catch(Exception e){ diagnostic.logWarning("Unable to get initial location mode: " + e.getMessage()); } super.initialize(cordova, webView); } /** * Called on destroying activity */ public void onDestroy() { try { diagnostic.applicationContext.unregisterReceiver(locationProviderChangedReceiver); }catch(Exception e){ diagnostic.logWarning("Unable to unregister Location Provider Change receiver: " + e.getMessage()); } } /** * Executes the request and returns PluginResult. * * @param action The action to execute. * @param args JSONArry of arguments for the plugin. * @param callbackContext The callback id used when calling back into JavaScript. * @return True if the action was valid, false if not. */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { Diagnostic.instance.currentContext = currentContext = callbackContext; try { if (action.equals("switchToLocationSettings")){ switchToLocationSettings(); callbackContext.success(); } else if(action.equals("isLocationAvailable")) { callbackContext.success(isGpsLocationAvailable() || isNetworkLocationAvailable() ? 1 : 0); } else if(action.equals("isLocationEnabled")) { callbackContext.success(isGpsLocationEnabled() || isNetworkLocationEnabled() ? 1 : 0); } else if(action.equals("isGpsLocationAvailable")) { callbackContext.success(isGpsLocationAvailable() ? 1 : 0); } else if(action.equals("isNetworkLocationAvailable")) { callbackContext.success(isNetworkLocationAvailable() ? 1 : 0); } else if(action.equals("isGpsLocationEnabled")) { callbackContext.success(isGpsLocationEnabled() ? 1 : 0); } else if(action.equals("isNetworkLocationEnabled")) { callbackContext.success(isNetworkLocationEnabled() ? 1 : 0); } else if(action.equals("getLocationMode")) { callbackContext.success(getLocationModeName()); } else if(action.equals("requestLocationAuthorization")) { requestLocationAuthorization(args, callbackContext); }else { diagnostic.handleError("Invalid action"); return false; } }catch(Exception e ) { diagnostic.handleError("Exception occurred: ".concat(e.getMessage())); return false; } return true; } public boolean isGpsLocationAvailable() throws Exception { boolean result = isGpsLocationEnabled() && isLocationAuthorized(); diagnostic.logDebug("GPS location available: " + result); return result; } public boolean isGpsLocationEnabled() throws Exception { int mode = getLocationMode(); boolean result = (mode == 3 || mode == 1); diagnostic.logDebug("GPS location setting enabled: " + result); return result; } public boolean isNetworkLocationAvailable() throws Exception { boolean result = isNetworkLocationEnabled() && isLocationAuthorized(); diagnostic.logDebug("Network location available: " + result); return result; } public boolean isNetworkLocationEnabled() throws Exception { int mode = getLocationMode(); boolean result = (mode == 3 || mode == 2); diagnostic.logDebug("Network location setting enabled: " + result); return result; } public String getLocationModeName() throws Exception { String modeName; int mode = getLocationMode(); switch(mode){ case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: modeName = LOCATION_MODE_HIGH_ACCURACY; break; case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: modeName = LOCATION_MODE_DEVICE_ONLY; break; case Settings.Secure.LOCATION_MODE_BATTERY_SAVING: modeName = LOCATION_MODE_BATTERY_SAVING; break; case Settings.Secure.LOCATION_MODE_OFF: modeName = LOCATION_MODE_OFF; break; default: modeName = LOCATION_MODE_UNKNOWN; } return modeName; } public void notifyLocationStateChange(){ try { String newMode = getLocationModeName(); if(!newMode.equals(currentLocationMode)){ diagnostic.logDebug("Location mode change to: " + newMode); diagnostic.executePluginJavascript("location._onLocationStateChange(\"" + newMode +"\");"); currentLocationMode = newMode; } }catch(Exception e){ diagnostic.logError("Error retrieving current location mode on location state change: "+e.toString()); } } public void switchToLocationSettings() { diagnostic.logDebug("Switch to Location Settings"); Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); cordova.getActivity().startActivity(settingsIntent); } public void requestLocationAuthorization(JSONArray args, CallbackContext callbackContext) throws Exception{ JSONArray permissionsToRequest = new JSONArray(); boolean shouldRequestBackground = args.getBoolean(0); boolean shouldRequestPrecise = args.getBoolean(1); permissionsToRequest.put(networkLocationPermission); if(shouldRequestPrecise || Build.VERSION.SDK_INT < 31){ permissionsToRequest.put(gpsLocationPermission); } if(shouldRequestBackground && Build.VERSION.SDK_INT >= 29 ){ permissionsToRequest.put(backgroundLocationPermission); } int requestId = Diagnostic.instance.storeContextByRequestId(callbackContext); Diagnostic.instance._requestRuntimePermissions(permissionsToRequest, requestId); PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } /************ * Internals ***********/ /** * Returns current location mode */ private int getLocationMode() throws Exception { int mode; if (Build.VERSION.SDK_INT >= 19 && Build.VERSION.SDK_INT < 28){ // Kitkat to Oreo, Settings.Secute.LOCATION_MODE was deprecated in Pie (https://developer.android.com/reference/android/provider/Settings.Secure#LOCATION_MODE) mode = Settings.Secure.getInt(this.cordova.getActivity().getContentResolver(), Settings.Secure.LOCATION_MODE); }else{ // Pre-Kitkat and post-Oreo if(isLocationProviderEnabled(LocationManager.GPS_PROVIDER) && isLocationProviderEnabled(LocationManager.NETWORK_PROVIDER)){ mode = 3; } else if(isLocationProviderEnabled(LocationManager.GPS_PROVIDER)){ mode = 1; } else if(isLocationProviderEnabled(LocationManager.NETWORK_PROVIDER)){ mode = 2; }else{ mode = 0; } } return mode; } private boolean isLocationAuthorized() throws Exception { boolean authorized = diagnostic.hasRuntimePermission(diagnostic.permissionsMap.get(gpsLocationPermission)) || diagnostic.hasRuntimePermission(diagnostic.permissionsMap.get(networkLocationPermission)); Log.v(TAG, "Location permission is "+(authorized ? "authorized" : "unauthorized")); return authorized; } private boolean isLocationProviderEnabled(String provider) { return locationManager.isProviderEnabled(provider); } /************ * Overrides ***********/ protected final BroadcastReceiver locationProviderChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { try { final String action = intent.getAction(); if(instance != null && action.equals(LocationManager.PROVIDERS_CHANGED_ACTION)){ Log.v(TAG, "onReceiveLocationProviderChange"); instance.notifyLocationStateChange(); } } catch (Exception e) { diagnostic.logError("Error receiving location provider state change: "+e.toString()); } } }; }