/* * Copyright (c) 2017 OpenLocate * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.openlocate.android.core; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings; import android.support.annotation.RequiresApi; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.util.Log; import com.google.android.gms.ads.identifier.AdvertisingIdClient; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.openlocate.android.exceptions.InvalidConfigurationException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; public class OpenLocate { private static final String TAG = OpenLocate.class.getSimpleName(); private static final int LOCATION_PERMISSION_REQUEST = 1001; private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000; private static OpenLocate sharedInstance = null; private Context context; private OpenLocateHelper openLocateHelper; private Configuration configuration; private AdvertisingIdClient.Info advertisingIdInfo; private OpenLocate(Configuration configuration) { this.context = configuration.context; this.openLocateHelper = new OpenLocateHelper(context, configuration); this.configuration = configuration; } public static OpenLocate initialize(Configuration configuration) { saveConfiguration(configuration); if (sharedInstance == null) { sharedInstance = new OpenLocate(configuration); } boolean trackingEnabled = SharedPreferenceUtils.getInstance(configuration.context).getBoolanValue(Constants.TRACKING_STATUS, false); if (trackingEnabled && hasLocationPermission(configuration.context) && sharedInstance.isGooglePlayServicesAvailable() == ConnectionResult.SUCCESS) { sharedInstance.onPermissionsGranted(); } return sharedInstance; } public static OpenLocate getInstance() throws IllegalStateException { if (sharedInstance == null) { throw new IllegalStateException("OpenLate SDK must be initialized using initialize method first"); } return sharedInstance; } public void startTracking(Activity activity) { if (configuration == null) { return; } int resultCode = isGooglePlayServicesAvailable(); if (resultCode != ConnectionResult.SUCCESS) { if (activity != null) { GoogleApiAvailability.getInstance().getErrorDialog(activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST).show(); } return; } SharedPreferenceUtils.getInstance(context).setValue(Constants.TRACKING_STATUS, true); if (hasLocationPermission(context)) { onPermissionsGranted(); } else if (activity != null) { ActivityCompat.requestPermissions( activity, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}, LOCATION_PERMISSION_REQUEST); startCheckingPermissionTask(); } else { Log.w(TAG, "Location Permission has not been accepted or prompted."); } } public void stopTracking() { SharedPreferenceUtils.getInstance(context).setValue(Constants.TRACKING_STATUS, false); openLocateHelper.stopTracking(); } public void updateConfiguration(OpenLocate.Configuration configuration) { saveConfiguration(configuration); this.configuration = configuration; this.openLocateHelper.updateConfiguration(configuration); } public boolean isTracking() { return SharedPreferenceUtils.getInstance(context).getBoolanValue(Constants.TRACKING_STATUS, false); } protected OpenLocate.Configuration getConfiguration() { return configuration; } protected AdvertisingIdClient.Info getAdvertisingIdInfo() { return advertisingIdInfo; } private int isGooglePlayServicesAvailable() { GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); return apiAvailability.isGooglePlayServicesAvailable(context); } private void startCheckingPermissionTask() { Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if (hasLocationPermission(context)) { onPermissionsGranted(); this.cancel(); } } }, 5 * 1000, 5 * 1000); } private void onPermissionsGranted() { FetchAdvertisingInfoTask task = new FetchAdvertisingInfoTask(context, new FetchAdvertisingInfoTaskCallback() { @Override public void onAdvertisingInfoTaskExecute(AdvertisingIdClient.Info info) { advertisingIdInfo = info; openLocateHelper.startTracking(); } }); task.execute(); } private static void saveConfiguration(Configuration configuration) throws InvalidConfigurationException { if (configuration.endpoints.isEmpty()) { String message = "Invalid configuration. Please configure a valid urls"; Log.e(TAG, message); throw new InvalidConfigurationException( message ); } try { String endpoins = Endpoint.toJson(configuration.endpoints); SharedPreferenceUtils.getInstance(configuration.context).setValue(Constants.ENDPOINTS_KEY, endpoins); } catch (JSONException e) { e.printStackTrace(); } } private static boolean hasLocationPermission(Context context) { return (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED); } public static final class Configuration implements Parcelable { Context context = null; private List<Endpoint> endpoints; private String serverUrl; private HashMap<String, String> headers; private long transmissionInterval; private long locationUpdateInterval; private LocationAccuracy locationAccuracy; private boolean isWifiCollectionDisabled; private boolean isDeviceModelCollectionDisabled; private boolean isDeviceManufacturerCollectionDisabled; private boolean isOperatingSystemCollectionDisbaled; private boolean isChargingInfoCollectionDisabled; private boolean isCarrierNameCollectionDisabled; private boolean isConnectionTypeCollectionDisabled; private boolean isLocationMethodCollectionDisabled; private boolean isLocationContextCollectionDisabled; public static final class Builder { private Context context; private List<Endpoint> endpoints; private String serverUrl; private HashMap<String, String> headers; private long transmissionInterval = Constants.DEFAULT_TRANSMISSION_INTERVAL_SEC; private long locationUpdateInterval = Constants.DEFAULT_LOCATION_INTERVAL_SEC; private LocationAccuracy locationAccuracy = Constants.DEFAULT_LOCATION_ACCURACY; private boolean isWifiCollectionDisabled; private boolean isDeviceModelCollectionDisabled; private boolean isDeviceManufacturerCollectionDisabled; private boolean isOperatingSystemCollectionDisbaled; private boolean isChargingInfoCollectionDisabled; private boolean isCarrierNameCollectionDisabled; private boolean isConnectionTypeCollectionDisabled; private boolean isLocationMethodCollectionDisabled; private boolean isLocationContextCollectionDisabled; public Builder(Context context, List<Endpoint> endpoints) { this.context = context.getApplicationContext(); this.endpoints = endpoints; } public Builder(Context context, String serverUrl) { this.context = context.getApplicationContext(); this.serverUrl = serverUrl; } public Builder setHeaders(HashMap<String, String> headers) { this.headers = headers; return this; } public Builder setTransmissionInterval(long seconds) { this.transmissionInterval = seconds; return this; } public Builder setLocationUpdateInterval(long seconds) { this.locationUpdateInterval = seconds; return this; } public Builder setLocationAccuracy(LocationAccuracy locationAccuracy) { this.locationAccuracy = locationAccuracy; return this; } public Builder withoutWifiInfo() { this.isWifiCollectionDisabled = true; return this; } public Builder withoutDeviceModel() { this.isDeviceModelCollectionDisabled = true; return this; } public Builder withoutDeviceManufacturer() { this.isDeviceManufacturerCollectionDisabled = true; return this; } public Builder withoutOperatingSystem() { this.isOperatingSystemCollectionDisbaled = true; return this; } public Builder withoutChargingInfo() { this.isChargingInfoCollectionDisabled = true; return this; } public Builder withoutCarrierName() { this.isCarrierNameCollectionDisabled = true; return this; } public Builder withoutConnectionType() { this.isConnectionTypeCollectionDisabled = true; return this; } public Builder withoutLocationMethod() { this.isLocationMethodCollectionDisabled = true; return this; } public Builder withoutLocationContext() { this.isLocationContextCollectionDisabled = true; return this; } public Configuration build() { if (serverUrl != null) { Endpoint endpoint = new Endpoint(serverUrl, headers); if (endpoints == null) { endpoints = new ArrayList<>(); } endpoints.add(endpoint); } return new Configuration(this); } } private Configuration(Builder builder) { this.context = builder.context; this.endpoints = builder.endpoints; this.transmissionInterval = builder.transmissionInterval; this.locationUpdateInterval = builder.locationUpdateInterval; this.locationAccuracy = builder.locationAccuracy; this.isCarrierNameCollectionDisabled = builder.isCarrierNameCollectionDisabled; this.isChargingInfoCollectionDisabled = builder.isChargingInfoCollectionDisabled; this.isConnectionTypeCollectionDisabled = builder.isConnectionTypeCollectionDisabled; this.isDeviceManufacturerCollectionDisabled = builder.isDeviceManufacturerCollectionDisabled; this.isDeviceModelCollectionDisabled = builder.isDeviceModelCollectionDisabled; this.isLocationContextCollectionDisabled = builder.isLocationContextCollectionDisabled; this.isLocationMethodCollectionDisabled = builder.isLocationMethodCollectionDisabled; this.isOperatingSystemCollectionDisbaled = builder.isOperatingSystemCollectionDisbaled; this.isWifiCollectionDisabled = builder.isWifiCollectionDisabled; } public List<Endpoint> getEndpoints() { return endpoints; } public long getTransmissionInterval() { return transmissionInterval; } public long getLocationUpdateInterval() { return locationUpdateInterval; } public LocationAccuracy getLocationAccuracy() { return locationAccuracy; } public boolean isWifiCollectionDisabled() { return isWifiCollectionDisabled; } public boolean isDeviceModelCollectionDisabled() { return isDeviceModelCollectionDisabled; } public boolean isDeviceManufacturerCollectionDisabled() { return isDeviceManufacturerCollectionDisabled; } public boolean isOperaringSystemCollectionDisbaled() { return isOperatingSystemCollectionDisbaled; } public boolean isChargingInfoCollectionDisabled() { return isChargingInfoCollectionDisabled; } public boolean isCarrierNameCollectionDisabled() { return isCarrierNameCollectionDisabled; } public boolean isConnectionTypeCollectionDisabled() { return isConnectionTypeCollectionDisabled; } public boolean isLocationMethodCollectionDisabled() { return isLocationMethodCollectionDisabled; } public boolean isLocationContextCollectionDisabled() { return isLocationContextCollectionDisabled; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeTypedList(this.endpoints); dest.writeString(this.serverUrl); dest.writeSerializable(this.headers); dest.writeLong(this.transmissionInterval); dest.writeLong(this.locationUpdateInterval); dest.writeInt(this.locationAccuracy == null ? -1 : this.locationAccuracy.ordinal()); dest.writeByte(this.isWifiCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isDeviceModelCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isDeviceManufacturerCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isOperatingSystemCollectionDisbaled ? (byte) 1 : (byte) 0); dest.writeByte(this.isChargingInfoCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isCarrierNameCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isConnectionTypeCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isLocationMethodCollectionDisabled ? (byte) 1 : (byte) 0); dest.writeByte(this.isLocationContextCollectionDisabled ? (byte) 1 : (byte) 0); } protected Configuration(Parcel in) { this.endpoints = in.createTypedArrayList(Endpoint.CREATOR); this.serverUrl = in.readString(); this.headers = (HashMap<String, String>) in.readSerializable(); this.transmissionInterval = in.readLong(); this.locationUpdateInterval = in.readLong(); int tmpLocationAccuracy = in.readInt(); this.locationAccuracy = tmpLocationAccuracy == -1 ? null : LocationAccuracy.values()[tmpLocationAccuracy]; this.isWifiCollectionDisabled = in.readByte() != 0; this.isDeviceModelCollectionDisabled = in.readByte() != 0; this.isDeviceManufacturerCollectionDisabled = in.readByte() != 0; this.isOperatingSystemCollectionDisbaled = in.readByte() != 0; this.isChargingInfoCollectionDisabled = in.readByte() != 0; this.isCarrierNameCollectionDisabled = in.readByte() != 0; this.isConnectionTypeCollectionDisabled = in.readByte() != 0; this.isLocationMethodCollectionDisabled = in.readByte() != 0; this.isLocationContextCollectionDisabled = in.readByte() != 0; } public static final Creator<Configuration> CREATOR = new Creator<Configuration>() { @Override public Configuration createFromParcel(Parcel source) { return new Configuration(source); } @Override public Configuration[] newArray(int size) { return new Configuration[size]; } }; } public static class Endpoint implements Parcelable { public static final String URL = "url"; public static final String HEADERS = "headers"; public static final String HEADERS_KEY = "key"; public static final String HEADERS_VALUE = "value"; public static List<Endpoint> fromJson(String json) throws JSONException { JSONArray jsonArray = new JSONArray(json); List<Endpoint> result = new ArrayList<>(jsonArray.length()); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonEndpoint = jsonArray.getJSONObject(i); Builder builder = Endpoint.builder(jsonEndpoint.getString(URL)); JSONArray headers = jsonEndpoint.getJSONArray(HEADERS); for (int j = 0; j < headers.length(); j++) { JSONObject header = headers.getJSONObject(j); builder.withHeader(header.getString(HEADERS_KEY), header.getString(HEADERS_VALUE)); } result.add(builder.build()); } return result; } public static String toJson(List<Endpoint> endpoints) throws JSONException { JSONArray jsonArray = new JSONArray(); for (Endpoint endpoint : endpoints) { JSONObject jsonEndpoint = new JSONObject(); jsonEndpoint.put(URL, endpoint.url); JSONArray jsonHeaders = new JSONArray(); for (Map.Entry<String, String> entry : endpoint.getHeaders().entrySet()) { JSONObject header = new JSONObject(); header.put(HEADERS_KEY, entry.getKey()); header.put(HEADERS_VALUE, entry.getValue()); jsonHeaders.put(header); } jsonEndpoint.put(HEADERS, jsonHeaders); jsonArray.put(jsonEndpoint); } return jsonArray.toString(); } private String url; private HashMap<String, String> headers; public Endpoint(String url, HashMap<String, String> headers) { this.url = url; if (headers == null) { this.headers = new HashMap<>(); } else { this.headers = headers; } } private Endpoint(Builder builder) { this.url = builder.url; this.headers = builder.headers; } public String getUrl() { return url; } public HashMap<String, String> getHeaders() { return headers; } public static Builder builder(String url) { return new Builder(url); } public static class Builder { private String url; private HashMap<String, String> headers; public Builder(String url) { this.url = url; } public Builder withHeader(String key, String value) { if (headers == null) { headers = new HashMap<>(); } headers.put(key, value); return this; } public Builder withHeaders(Map<String, String> headers) { if (this.headers == null) { this.headers = new HashMap<>(); } this.headers.putAll(headers); return this; } public Endpoint build() { return new Endpoint(this.url, this.headers); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.url); dest.writeInt(this.headers.size()); for (Map.Entry<String, String> entry : this.headers.entrySet()) { dest.writeString(entry.getKey()); dest.writeString(entry.getValue()); } } protected Endpoint(Parcel in) { this.url = in.readString(); int headersSize = in.readInt(); this.headers = new HashMap<String, String>(headersSize); for (int i = 0; i < headersSize; i++) { String key = in.readString(); String value = in.readString(); this.headers.put(key, value); } } public static final Creator<Endpoint> CREATOR = new Creator<Endpoint>() { @Override public Endpoint createFromParcel(Parcel source) { return new Endpoint(source); } @Override public Endpoint[] newArray(int size) { return new Endpoint[size]; } }; @Override public String toString() { return "{url:" + url + "}"; } } }