Java tutorial
package mobi.lab.sample_event_logging_library.service; /** * This file is part of sample-logging-library. * * sample-logging-library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * sample-logging-library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with sample-logging-library. If not, see <http://www.gnu.org/licenses/>. */ import android.Manifest; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.util.Log; import com.squareup.okhttp.Interceptor; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Iterator; import java.util.TimeZone; import mobi.lab.sample_event_logging_library.Config; import mobi.lab.sample_event_logging_library.data.LogEvent; import mobi.lab.sample_event_logging_library.network.LogEventRequest; import mobi.lab.sample_event_logging_library.network.Network; import retrofit.Callback; import retrofit.GsonConverterFactory; import retrofit.Response; import retrofit.Retrofit; /** * Service for posting logs in background. Should run in a separate process (should be set so in Manifest)<br/> * What it does:<br/> * * It gets an log event and tries to add a location to it (if location service is not available then events will be just sent to server). * If no location is available (but location service is available) then it will first try to get a valid location and then upload events to server. * <br/><br/> * Events are meant to be uploaded in batches: if {@link mobi.lab.sample_event_logging_library.Config#EVENTS_PER_REQUEST_SOFT_LIMIT} event soft * limit is hit or time interval exceeds {@link mobi.lab.sample_event_logging_library.Config#MAX_TIME_INTERVAL_BETWEEN_REQUESTS} milliseconds. *<br/><br/> * * TODO #1: location service is not fully functional, we are not getting enough location updates which can cause log events with no location * TODO #2: handle no connection -> in case of no connection there is no point to initiate logs sync request * TODO #3: handle no location permission situation (dialogs for asking permission) * TODO #4 look over the handler logic for time interval -> maybe support JobScheduler as much as possible? * * To make use of this you have to declare this in the Manifest: {@code <service android:name="LogPostService" android:label="@string/app_name" android:process="mobi.lab.sample_event_logging_library.someOtherProcess" /> } * <br/> * You'll need {@code <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />} to get location. <br/> * You'll also need {@code <uses-permission android:name="android.permission.INTERNET" />} to post logs. <br/> * * TO */ public class LogPostService extends Service implements LocationListener { private static final String TAG = LogPostService.class.getSimpleName(); public static final String EXTRA_EVENT_TYPE = TAG + ".EXTRA_EVENT_TYPE"; public static final String EXTRA_EVENT_CONTENT = TAG + ".EXTRA_EVENT_CONTENT"; private ArrayList<LogEvent> logEvents = new ArrayList<>(); private final Object logEventsSyncLock = new Object(); private ArrayList<LogEvent> logEventsForRequest; private Handler handler; private LogEventRequest logEventService; private Runnable timedRunnable = new Runnable() { @Override public void run() { if (logEvents.size() > 0) { sendLogsToServer(); if (handler != null) { handler.postDelayed(timedRunnable, Config.MAX_TIME_INTERVAL_BETWEEN_REQUESTS); } } } }; private boolean isLocationServicesEnabled; private Location lastKnownLocation; public LogPostService() { } @Override public void onCreate() { super.onCreate(); //TODO: starting from Android 5.0 we could use JobScheduler (currently not in support libraries) handler = new Handler(); handler.postDelayed(timedRunnable, Config.MAX_TIME_INTERVAL_BETWEEN_REQUESTS); logEventsForRequest = new ArrayList<>(); OkHttpClient client = new OkHttpClient(); client.interceptors().add(new Interceptor() { @Override public com.squareup.okhttp.Response intercept(Chain chain) throws IOException { Request request = chain.request(); Log.d(TAG, String.format("\n\nrequest:%s\nheaders:%s\n" + "url:%s", request.body().toString(), request.headers(), request.httpUrl().toString())); return chain.proceed(request); } }); Retrofit retrofit = new Retrofit.Builder().baseUrl(Network.BASE_API_URL) .addConverterFactory(GsonConverterFactory.create()).client(client).build(); logEventService = retrofit.create(LogEventRequest.class); if (checkCallingOrSelfPermission( Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && checkCallingOrSelfPermission( Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { //TODO: ask for permission if not given Log.d(TAG, "location permission not granted"); isLocationServicesEnabled = false; return; } isLocationServicesEnabled = true; final LocationManager locationManager = (LocationManager) getApplicationContext() .getSystemService(Context.LOCATION_SERVICE); final Criteria criteria = new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE); Log.d(TAG, "requesting user location"); locationManager.requestLocationUpdates(Config.LOCATION_UPDATES_MIN_TIME, Config.LOCATION_UPDATES_MIN_DISTANCE, criteria, this, null); } @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind"); throw new UnsupportedOperationException("onBind not yet implemented"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // intent may be null if the service is being restarted after its process has gone away, // and it had previously returned anything except START_STICKY_COMPATIBILITY. if (intent != null && intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_EVENT_TYPE)) { Log.d(TAG, " onStartCommand"); int eventType = intent.getExtras().getInt(EXTRA_EVENT_TYPE); String eventContent = intent.getExtras().getString(EXTRA_EVENT_CONTENT); LogEvent event = new LogEvent(eventType, eventContent); if (isLocationGoodEnough(lastKnownLocation)) { event.setLocation(lastKnownLocation); } handleLogEvent(event); } return super.onStartCommand(intent, flags, startId); } private void handleLogEvent(LogEvent logEvent) { if (logEvent.getLocation() == null && isLocationServicesEnabled && !isLocationGoodEnough(lastKnownLocation)) { //let's wait until we get a good enough location logEvents.add(logEvent); return; } if (logEvents.size() + 1 >= Config.EVENTS_PER_REQUEST_SOFT_LIMIT) { sendLogsToServer(); } else { logEvents.add(logEvent); } } private void sendLogsToServer() { synchronized (logEventsSyncLock) { //check for running requests if (logEventsForRequest.size() == 0) { //if location services are enabled then we will only send events with location and wait for the location if (isLocationServicesEnabled) { for (Iterator<LogEvent> iterator = logEvents.iterator(); iterator.hasNext();) { LogEvent logEvent = iterator.next(); if (logEvent.getLocation() != null || logEvent.getNrOfTriesForLocation() >= Config.NR_OF_GET_LOCATION_TRIES) { Log.d(TAG, "sendLogsToServer -> queueing log: " + logEvent.toString()); logEventsForRequest.add(logEvent); iterator.remove(); } else { logEvent.increaseNrOfTriesForLocation(); } } } else { //if location services are disabled then we will send even the events without the location Log.d(TAG, "sendLogsToServer -> location services are disabled, will sync all(" + logEvents.size() + ") logEvents"); logEventsForRequest.addAll(logEvents); logEvents.clear(); } if (logEventsForRequest.size() > 0) { logEventService.saveLogEvents(logEventsForRequest).enqueue(new Callback<Void>() { @Override public void onResponse(Response<Void> response) { synchronized (logEventsSyncLock) { if (response.code() >= 200 && response.code() < 300) { Log.d(TAG, logEventsForRequest.size() + " events synced to server [" + logEvents.size() + " events waiting for sync]"); logEventsForRequest.clear(); stopServiceIfEventsHandled(); } else { logEvents.addAll(logEventsForRequest); logEventsForRequest.clear(); //something went wrong, let's try again later stopServiceIfEventsHandled(); } } } @Override public void onFailure(Throwable t) { synchronized (logEventsSyncLock) { logEvents.addAll(logEventsForRequest); logEventsForRequest.clear(); //something went wrong, probably network error -> we will try again later stopServiceIfEventsHandled(); } } }); } else { Log.d(TAG, "no logEvents to sync -> " + logEvents.size() + " events waiting for location"); stopServiceIfEventsHandled(); } } else { stopServiceIfEventsHandled(); } } } private void stopServiceIfEventsHandled() { if (logEvents.size() == 0) { Log.d(TAG, "stopService()"); stopSelf(); } } @Override public void onLocationChanged(Location location) { if (!isLocationGoodEnough(location)) { Log.d(TAG, "onLocationChanged: Skipping " + location + ", provider: " + location.getProvider() + ", not good enough"); return; } Log.d(TAG, "onLocationChanged: updating to " + location + ", provider: " + location.getProvider()); lastKnownLocation = location; synchronized (logEventsSyncLock) { for (LogEvent logEvent : logEvents) { if (logEvent.getLocation() == null) { Log.d(TAG, "updating location of logEvent: " + logEvent.toString()); logEvent.setLocation(lastKnownLocation); } } } } @Override public void onStatusChanged(String s, int i, Bundle bundle) { } @Override public void onProviderEnabled(String s) { } @Override public void onProviderDisabled(String s) { } private boolean isLocationGoodEnough(final Location location) { if (location == null) { return false; } if (location.hasAccuracy() && location.getAccuracy() >= Config.LOCATION_IS_NOT_ACCURATE) { Log.d(TAG, "isLocationGoodEnough: false, location accuracy " + location.getAccuracy() + "m >= " + Config.LOCATION_IS_NOT_ACCURATE + "m"); return false; } final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); final long locationTime = location.getTime(); final long timeDifference = cal.getTime().getTime() - locationTime; if (timeDifference >= Config.LOCATION_IS_OLD) { Log.d(TAG, "isLocationGoodEnough: false, location age >= " + Config.LOCATION_IS_OLD + " ms"); return false; } return true; } }