Java tutorial
/* * Copyright (C) 2015 carlosperate http://carlosperate.github.io * * 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.embeddedlog.LightUpDroid; import android.app.Activity; import android.app.ProgressDialog; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.media.RingtoneManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.preference.PreferenceManager; import android.widget.Toast; import com.embeddedlog.LightUpDroid.provider.Alarm; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Calendar; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * Synchronises the Alarms with the LightUpPi server alarms by providing methods to retrieve, edit, * add and delete alarms on the server. */ public class LightUpPiSync { private static final String LOG_TAG = "LightUpPiSync: "; private Context mActivityContext; private AlarmClockFragment mAlarmFragment; // Used to schedule a permanently running background LightUpPi server check private ScheduledExecutorService scheduleServerCheck; // Defines the types of tasks that is required to be performed public enum TaskType { SYNC, PUSH_TO_PHONE, PUSH_TO_SERVER, GET_ALARM, ADD_ALARM, EDIT_ALARM, DELETE_ALARM, } /** * Public constructor. Saves the class context to be able to check the network connectivity * and display a progress dialog. * * @param activityContext Context of the activity (no application context) requesting the sync. */ public LightUpPiSync(Context activityContext, String alarmFragmentTag) { this.mActivityContext = activityContext; Activity activity = (Activity) this.mActivityContext; this.mAlarmFragment = (AlarmClockFragment) activity.getFragmentManager() .findFragmentByTag(alarmFragmentTag); } /** * Synchronisation procedure to push all alarms from the LightUpPi server onto the phone. */ public void syncPushToPhone() { Uri.Builder allAlarmsUri = getServerUriBuilder(); allAlarmsUri.appendPath("getAlarm").appendQueryParameter("id", "all"); getJsonHandler(allAlarmsUri, TaskType.PUSH_TO_PHONE, Alarm.INVALID_ID); } private void syncPushToPhoneCallback(JSONObject jAllAlarms) { JSONArray jAlarms; List<Alarm> serverAlarms = new LinkedList<Alarm>(); try { jAlarms = jAllAlarms.getJSONArray("alarms"); for (int i = 0; i < jAlarms.length(); i++) { serverAlarms.add(alarmFromJson(jAlarms.getJSONObject(i))); if (Log.LOGV) Log.v("Alarm from server: " + serverAlarms.get(i).toString()); } } catch (Exception e) { if ((e instanceof JSONException) || (e instanceof NullPointerException)) { launchToast(R.string.lightuppi_sync_fail); Log.w(LOG_TAG + "Exception reading callback from push to phone operation: " + e); return; } else { throw new RuntimeException(e); } } // Get local alarms, passing null as selection argument retrieves all ContentResolver cr = mActivityContext.getContentResolver(); List<Alarm> localAlarms = Alarm.getAlarms(cr, null); // Now we have lists of all the alarms, because we are pushing to the phone check the // current local alarms and remove them if they are not in the list for (Alarm localAlarm : localAlarms) { toNextLocalAlarmIteration: { for (Alarm serverAlarm : serverAlarms) { if (localAlarm.lightuppiId == serverAlarm.lightuppiId) { // Because we are pushing to the phone update the alarm to whatever is in // the server, including the server timestamp copyAndroidProperties(localAlarm, serverAlarm); mAlarmFragment.asyncUpdateAlarm(serverAlarm, false, true); serverAlarms.remove(serverAlarm); break toNextLocalAlarmIteration; } } // This is only executed if a match between server and local alarm was not found mAlarmFragment.asyncDeleteAlarm(localAlarm, null, true); } } // The rest of the serverAlarms are new to the phone and present in the server for (Alarm serverAlarm : serverAlarms) { mAlarmFragment.asyncAddAlarm(serverAlarm, true); } } /** * Adds an alarm to the LightUpPi server. * * @param alarm New Alarm to add to LightUpPi server. */ public void addServerAlarm(Alarm alarm) { // First check if alarm has no associated LightUpPi server ID if (alarm.lightuppiId == Alarm.INVALID_ID) { Uri.Builder addAlarmUri = getServerUriBuilder(); addAlarmUri.appendPath("addAlarm").appendQueryParameter("hour", Integer.toString(alarm.hour)) .appendQueryParameter("minute", Integer.toString(alarm.minutes)) .appendQueryParameter("monday", Boolean.toString(alarm.daysOfWeek.isMondayEnabled())) .appendQueryParameter("tuesday", Boolean.toString(alarm.daysOfWeek.isTuesdayEnabled())) .appendQueryParameter("wednesday", Boolean.toString(alarm.daysOfWeek.isWednesdayEnabled())) .appendQueryParameter("thursday", Boolean.toString(alarm.daysOfWeek.isThursdayEnabled())) .appendQueryParameter("friday", Boolean.toString(alarm.daysOfWeek.isFridayEnabled())) .appendQueryParameter("saturday", Boolean.toString(alarm.daysOfWeek.isSaturdayEnabled())) .appendQueryParameter("sunday", Boolean.toString(alarm.daysOfWeek.isSundayEnabled())) .appendQueryParameter("enabled", Boolean.toString(alarm.enabled)) .appendQueryParameter("label", alarm.label) .appendQueryParameter("timestamp", Long.toString(alarm.timestamp)); getJsonHandler(addAlarmUri, TaskType.ADD_ALARM, alarm.id); } else { launchToast(R.string.lightuppi_add_existing); } } private void addServerAlarmCallback(long alarmID, JSONObject jResult) { boolean addSuccess; long lightuppiId; try { addSuccess = jResult.getBoolean("success"); lightuppiId = jResult.getLong("id"); } catch (Exception e) { if ((e instanceof JSONException) || (e instanceof NullPointerException)) { Log.w(LOG_TAG + "Exception when reading callback from add operation: " + e); launchToast(R.string.lightuppi_add_unsuccessful); return; } else { throw new RuntimeException(e); } } if (addSuccess) { // We need the alarm back before we can edit the LightUpPi ID ContentResolver cr = mActivityContext.getContentResolver(); Alarm addedAlarm = Alarm.getAlarm(cr, alarmID); addedAlarm.lightuppiId = lightuppiId; // Last argument causes the bypass of the Alarm.updateAlarm() automatic timestamp // and the edit of the alarm in the LightUpPi server mAlarmFragment.asyncUpdateAlarm(addedAlarm, false, true); launchToast(R.string.lightuppi_add_successful); } else { launchToast(R.string.lightuppi_add_unsuccessful); } } /** * Edits an alarm from the LightUpPi server. * * @param alarm LightUpPi Alarm to edit. */ public void editServerAlarm(Alarm alarm) { // First check if alarm has an associated LightUpPi server ID if (alarm.lightuppiId != Alarm.INVALID_ID) { Uri.Builder editAlarmUri = getServerUriBuilder(); editAlarmUri.appendPath("editAlarm").appendQueryParameter("id", Long.toString(alarm.lightuppiId)) .appendQueryParameter("hour", Integer.toString(alarm.hour)) .appendQueryParameter("minute", Integer.toString(alarm.minutes)) .appendQueryParameter("monday", Boolean.toString(alarm.daysOfWeek.isMondayEnabled())) .appendQueryParameter("tuesday", Boolean.toString(alarm.daysOfWeek.isTuesdayEnabled())) .appendQueryParameter("wednesday", Boolean.toString(alarm.daysOfWeek.isWednesdayEnabled())) .appendQueryParameter("thursday", Boolean.toString(alarm.daysOfWeek.isThursdayEnabled())) .appendQueryParameter("friday", Boolean.toString(alarm.daysOfWeek.isFridayEnabled())) .appendQueryParameter("saturday", Boolean.toString(alarm.daysOfWeek.isSaturdayEnabled())) .appendQueryParameter("sunday", Boolean.toString(alarm.daysOfWeek.isSundayEnabled())) .appendQueryParameter("enabled", Boolean.toString(alarm.enabled)) .appendQueryParameter("label", alarm.label); getJsonHandler(editAlarmUri, TaskType.EDIT_ALARM, alarm.id); } else { launchToast(R.string.lightuppi_no_server_ID); } } private void editServerAlarmCallback(JSONObject jResult) { boolean editSuccess; long lightuppiId; long newTimestamp; try { editSuccess = jResult.getBoolean("success"); lightuppiId = jResult.getLong("id"); newTimestamp = jResult.getLong("timestamp"); } catch (Exception e) { if ((e instanceof JSONException) || (e instanceof NullPointerException)) { Log.w(LOG_TAG + "Exception when reading callback from edit operation: " + e); launchToast(R.string.lightuppi_edit_unsuccessful); return; } else { throw new RuntimeException(e); } } if (editSuccess) { // We need the alarm back before we can edit the timestamp ContentResolver cr = mActivityContext.getContentResolver(); Alarm editedAlarm = Alarm.getAlarmLightuppiId(cr, lightuppiId); editedAlarm.timestamp = newTimestamp; // Last argument causes the bypass of the Alarm.updateAlarm() automatic timestamp // and the edit of the alarm in the LightUpPi server mAlarmFragment.asyncUpdateAlarm(editedAlarm, false, true); launchToast(R.string.lightuppi_edit_successful); } else { launchToast(R.string.lightuppi_edit_unsuccessful); } } /** * Deletes an alarm from the LightUpPi server. * * @param alarm LightUpPi Alarm to delete. */ public void deleteServerAlarm(Alarm alarm) { // First check if alarm has an associated LightUpPi server ID if (alarm.lightuppiId != Alarm.INVALID_ID) { Uri.Builder deleteAlarmUri = getServerUriBuilder(); deleteAlarmUri.appendPath("deleteAlarm").appendQueryParameter("id", Long.toString(alarm.lightuppiId)); getJsonHandler(deleteAlarmUri, TaskType.DELETE_ALARM, alarm.id); } else { launchToast(R.string.lightuppi_no_server_ID); } } private void deleteServerAlarmCallback(JSONObject jResult) { boolean deleteSuccess; try { deleteSuccess = jResult.getBoolean("success"); } catch (Exception e) { if ((e instanceof JSONException) || (e instanceof NullPointerException)) { Log.w(LOG_TAG + "Exception when reading callback from delete operation: " + e); launchToast(R.string.lightuppi_delete_unsuccessful); return; } else { throw new RuntimeException(e); } } if (deleteSuccess) { launchToast(R.string.lightuppi_delete_successful); } else { launchToast(R.string.lightuppi_delete_unsuccessful); } } /** * Gets the LightUpPi server IP from the settings and returns the server address string. * * @return Sever address to the LightUpPi app root folder. */ private Uri.Builder getServerUriBuilder() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivityContext); String serverIP = prefs.getString(SettingsActivity.KEY_LIGHTUPPI_SERVER, ""); // The LightUpPi server application runs through the LightUpPi directory Uri.Builder serverUriBuilder = new Uri.Builder(); serverUriBuilder.scheme("http").authority(serverIP).appendPath("LightUpPi"); if (Log.LOGV) Log.v(LOG_TAG + "LightUpPi server " + serverUriBuilder.build().toString()); return serverUriBuilder; } /** * Every request is handled by this method, which launches an async task to retrieve the data. * Before attempting to fetch the URL, makes sure that there is a network connection. * * @param uriBuilder The URI Builder of the JSON data address to request. * @param taskType Indicates which task it is to be performed. * @param alarmId If applicable, the Alarm ID (local, not LightUpPi Id) to perform the task. */ private void getJsonHandler(Uri.Builder uriBuilder, TaskType taskType, long alarmId) { // First check if there is network connectivity ConnectivityManager connMgr = (ConnectivityManager) mActivityContext .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { String urlString = uriBuilder.build().toString(); new DownloadJsonTask(taskType, alarmId).execute(urlString); } else { launchToast(R.string.lightuppi_no_connection); } } /** * Uses AsyncTask to create a task away from the main UI thread, where the wrapper class is * called from. This task takes a URL string and uses it to create an HttpUrlConnection. * Once the connection has been established, the AsyncTask downloads the contents of the * web page as an InputStream. Finally, the InputStream is converted into a string, which is * displayed in the UI by the AsyncTask's onPostExecute method. */ private class DownloadJsonTask extends AsyncTask<String, Void, JSONObject> { private ProgressDialog progress; private TaskType mTaskType; private long mAlarmId; /** * Constructor requires a TaskType argument to identify the correct callback. * * @param taskType The type of task required in order to identify the right callback. */ DownloadJsonTask(TaskType taskType, long alarmId) { this.mTaskType = taskType; this.mAlarmId = alarmId; } /** * Launches the progress dialog while the data is being retrieved. * It is dismissed on onPostExecute. */ @Override protected void onPreExecute() { ((Activity) mActivityContext).runOnUiThread(new Runnable() { public void run() { progress = ProgressDialog.show(mActivityContext, null, mActivityContext.getString(R.string.lightuppi_syncing_message), true); } }); } /** * @param urls String array with the URL to retrieve JSON from, only first array item used. * @return JSON from server in JSONObject format. */ @Override protected JSONObject doInBackground(String... urls) { String jsonStr; try { // Only expecting 1 url parameter, overwrite requires the array to be maintained jsonStr = getJsonFrom(urls[0]); } catch (IOException e) { Log.w(LOG_TAG + "JSONException: " + e.toString()); // Error dealt with in the callback from onPostExecute, by passing null object return null; } if (Log.LOGV) Log.v(LOG_TAG + jsonStr); JSONObject wrapperJsonObject = null; try { wrapperJsonObject = new JSONObject(jsonStr); } catch (JSONException e) { Log.w(LOG_TAG + "JSONException: " + e.toString()); // Error dealt with in the callback from onPostExecute, by passing null object } return wrapperJsonObject; } /** Closes the progress dialog and sends the data to the relevant callback. */ @Override protected void onPostExecute(JSONObject result) { // Select callback based on task type switch (mTaskType) { case SYNC: break; case PUSH_TO_SERVER: break; case PUSH_TO_PHONE: syncPushToPhoneCallback(result); break; case GET_ALARM: break; case ADD_ALARM: addServerAlarmCallback(mAlarmId, result); break; case EDIT_ALARM: editServerAlarmCallback(result); break; case DELETE_ALARM: deleteServerAlarmCallback(result); break; default: Log.w(LOG_TAG + "Coding bug, there was no callback defined for the task " + mTaskType.toString()); break; } // Close the progress dialog if applicable, cases need to be the same as onPreExecute progress.dismiss(); } /** * Given a URL, establishes an HttpUrlConnection and retrieves the content as a * InputStream, which it returns as a string. * * @param urlStr String array containing as the first argument the URL to retrieve data. * @return String with the URL data * @throws IOException */ private String getJsonFrom(String urlStr) throws IOException { InputStream is = null; try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); /* milliseconds */ conn.setConnectTimeout(5000); /* milliseconds */ conn.setRequestMethod("GET"); conn.setDoInput(true); // Starts the query conn.connect(); int response = conn.getResponseCode(); if (response == 500) { launchToast(R.string.lightuppi_response_500); } else if (response != 200) { launchToast(String.format(mActivityContext.getString(R.string.lightuppi_response_not_200), response)); } // Get and convert the InputStream into a string is = conn.getInputStream(); return stringFromStream(is); } finally { // Ensure InputStream is closed after the app is finished using it. if (is != null) is.close(); } } /** * Converts the input stream from the web content into an String. * * @param stream InputStream to be converted into String. * @return String with the stream parameter data. * @throws IOException */ private String stringFromStream(InputStream stream) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); StringBuilder out = new StringBuilder(); String newLine = System.getProperty("line.separator"); String line; while ((line = reader.readLine()) != null) { out.append(line); out.append(newLine); } return out.toString(); } } private Alarm alarmFromJson(JSONObject aJson) { // If mSelectedAlarm is null then we're creating a new alarm. Alarm alarm = new Alarm(); alarm.alert = RingtoneManager.getActualDefaultRingtoneUri(mActivityContext, RingtoneManager.TYPE_ALARM); if (alarm.alert == null) { alarm.alert = Uri.parse("content://settings/system/alarm_alert"); } // Setting the vibrate option to always true, as there is no attribute in LightUpPi alarm.vibrate = true; // Setting the 'delete after use' option to always false, as there is no such feature in // the LightUpPi alarm system and all alarms are repeatable alarm.deleteAfterUse = false; // Parsing the JSON data try { alarm.hour = aJson.getInt("hour"); alarm.minutes = aJson.getInt("minute"); alarm.enabled = aJson.getBoolean("enabled"); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("monday"), Calendar.MONDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("tuesday"), Calendar.TUESDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("wednesday"), Calendar.WEDNESDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("thursday"), Calendar.THURSDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("friday"), Calendar.FRIDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("saturday"), Calendar.SATURDAY); alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("sunday"), Calendar.SUNDAY); alarm.label = aJson.getString("label"); alarm.lightuppiId = aJson.getLong("id"); alarm.timestamp = aJson.getLong("timestamp"); } catch (JSONException e) { Log.w(LOG_TAG + " JSONException: " + e.toString()); alarm = null; } return alarm; } /** * Because the LightUpDrop Alarms have more data than the LightUpPi Alarms this method is used * to copy the properties over from one alarm to the other. * * @param droid Alarm local to the phone. * @param pi Alarm coming from the LightUpPi server. */ private void copyAndroidProperties(Alarm droid, Alarm pi) { pi.id = droid.id; pi.alert = droid.alert; pi.vibrate = droid.vibrate; pi.deleteAfterUse = droid.deleteAfterUse; } /** * Initiates a background thread to check if the LightUpPi server is reachable. * * @param guiHandler Handler for the activity GUI, for which to send one of the two runnables. * @param online Runnable to execute in the Handler if the server is online. * @param offline Runnable to execute in the Handler if the server is offline. */ public void startBackgroundServerCheck(final Handler guiHandler, final Runnable online, final Runnable offline) { // Check for network connectivity ConnectivityManager connMgr = (ConnectivityManager) mActivityContext .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if ((networkInfo != null) && networkInfo.isConnected() && ((scheduleServerCheck == null) || scheduleServerCheck.isShutdown())) { // Get the ping address final Uri.Builder pingUri = getServerUriBuilder(); pingUri.appendPath("ping"); // Schedule the background server check scheduleServerCheck = Executors.newScheduledThreadPool(1); scheduleServerCheck.scheduleWithFixedDelay(new Runnable() { public void run() { int response = 0; try { URL url = new URL(pingUri.build().toString()); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); /* milliseconds */ conn.setConnectTimeout(5000); /* milliseconds */ conn.setRequestMethod("GET"); conn.setDoInput(true); conn.connect(); response = conn.getResponseCode(); } catch (Exception e) { // Safely ignored as a response!=200 will trigger the offline title } if (response == 200) { if (Log.LOGV) Log.i(LOG_TAG + "Server response 200"); guiHandler.post(online); } else { if (Log.LOGV) Log.i(LOG_TAG + "Server response NOT 200"); guiHandler.post(offline); } } }, 0, 30, TimeUnit.SECONDS); if (Log.LOGV) Log.v(LOG_TAG + "BackgroundServerCheck started"); } else { if (Log.LOGV) Log.d(LOG_TAG + "Server response NOT 200"); guiHandler.post(offline); } } /** Stops the background server check */ public void stopBackgroundServerCheck() { try { scheduleServerCheck.shutdown(); if (Log.LOGV) Log.v(LOG_TAG + "BackgroundServerCheck stopped"); } catch (NullPointerException e) { // This will be triggered due to the network being unavailable, safe to ignore } } /** Launches a Toast in the main gui thread */ private void launchToast(final int resourceId) { ((Activity) mActivityContext).runOnUiThread(new Runnable() { public void run() { Toast.makeText(mActivityContext, resourceId, Toast.LENGTH_LONG).show(); } }); } private void launchToast(final String toastText) { ((Activity) mActivityContext).runOnUiThread(new Runnable() { public void run() { Toast.makeText(mActivityContext, toastText, Toast.LENGTH_LONG).show(); } }); } }