Java tutorial
/* * Numenta Platform for Intelligent Computing (NuPIC) * Copyright (C) 2015, Numenta, Inc. Unless you have purchased from * Numenta, Inc. a separate commercial license for this software code, the * following terms and conditions apply: * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero Public License version 3 as * published by the Free Software Foundation. * * This program 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 Affero Public License for more details. * * You should have received a copy of the GNU Affero Public License * along with this program. If not, see http://www.gnu.org/licenses. * * http://numenta.org/licenses/ * */ package com.numenta.core.service; import com.numenta.core.R; import com.numenta.core.app.HTMApplication; import com.numenta.core.data.Annotation; import com.numenta.core.data.CoreDatabase; import com.numenta.core.data.Metric; import com.numenta.core.utils.DataUtils; import com.numenta.core.utils.Log; import com.numenta.core.utils.NetUtils; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Looper; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; import android.text.format.DateUtils; import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import static com.numenta.core.preference.PreferencesConstants.PREF_DATA_REFRESH_RATE; import static com.numenta.core.preference.PreferencesConstants.PREF_LAST_CONNECTED_TIME; /** * This service is managed by {@link DataService} and is responsible for * synchronizing the local metric database with the server. It will poll the * server at {@link #REFRESH_RATE} interval for new data and download all * available data since last update. */ public class DataSyncService { /** * This Event is fired on metric data changes */ public static final String METRIC_DATA_CHANGED_EVENT = "com.numenta.core.data.MetricDataChangedEvent"; /** * This Event is fired on metric changes */ public static final String METRIC_CHANGED_EVENT = "com.numenta.core.data.MetricChangedEvent"; /** * This Event is fired on annotations changes */ public static final String ANNOTATION_CHANGED_EVENT = "com.numenta.core.data.AnnotationChangedEvent"; /** * This Event is fired when the server starts and stops downloading data. Check event's <b> * <code>isRefreshing</code></b> parameter for refreshing status. */ public static final String REFRESH_STATE_EVENT = "com.numenta.core.data.RefreshStateEvent"; /** * Default Refresh rate in minutes. User may override using application settings */ public static final String REFRESH_RATE = "5"; // Handles user preferences changes private final OnSharedPreferenceChangeListener _preferenceChangeListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(final SharedPreferences prefs, String key) { if (key.equals(PREF_DATA_REFRESH_RATE)) { scheduleUpdate(Long.parseLong(prefs.getString(PREF_DATA_REFRESH_RATE, REFRESH_RATE))); } } }; private static final String TAG = DataSyncService.class.getSimpleName(); // Main Service private final DataService _service; // This task will periodically load data from the server private ScheduledFuture<?> _updateTask; // HTM API Helper private HTMClient _htmClient; // Prevent multiple threads from downloading data from the server // simultaneously private volatile boolean _synchronizingWithServer; /** * DataSyncService constructor. * <p> * Should only be called by {@link DataService} * </p> * * @param service The main {@link DataService} */ /* package */ public DataSyncService(DataService service) { this._service = service; } /** * Fire {@link DataSyncService#REFRESH_STATE_EVENT} */ protected void fireRefreshStateEvent(boolean isRefreshing) { Intent intent = new Intent(DataSyncService.REFRESH_STATE_EVENT); intent.putExtra("isRefreshing", isRefreshing); LocalBroadcastManager.getInstance(_service).sendBroadcast(intent); HTMApplication.setLastError(null); } /** * Fire {@link DataSyncService#REFRESH_STATE_EVENT} */ protected void fireRefreshStateEvent(boolean isRefreshing, String result) { Intent intent = new Intent(DataSyncService.REFRESH_STATE_EVENT); intent.putExtra("isRefreshing", isRefreshing); LocalBroadcastManager.getInstance(_service).sendBroadcast(intent); HTMApplication.setLastError(result); } /** * Fire {@link DataSyncService#METRIC_CHANGED_EVENT} */ protected void fireMetricChangedEvent() { Log.d(TAG, "Metric changed"); Intent intent = new Intent(DataSyncService.METRIC_CHANGED_EVENT); LocalBroadcastManager.getInstance(_service).sendBroadcast(intent); } /** * Fire {@link DataSyncService#METRIC_DATA_CHANGED_EVENT} */ protected void fireMetricDataChangedEvent() { Log.d(TAG, "Metric Data changed"); Intent intent = new Intent(METRIC_DATA_CHANGED_EVENT); LocalBroadcastManager.getInstance(_service).sendBroadcast(intent); } /** * Fire {@link #ANNOTATION_CHANGED_EVENT} */ protected void fireAnnotationChangedEvent() { Log.d(TAG, "Annotation changed"); Intent intent = new Intent(ANNOTATION_CHANGED_EVENT); LocalBroadcastManager.getInstance(_service).sendBroadcast(intent); } /** * Load all metrics from server and update the local database by adding new metrics and removing * old ones. This method will fire {@link #METRIC_CHANGED_EVENT} * * @return Number of new metrics */ protected int loadAllMetrics() throws InterruptedException, ExecutionException, HTMException, IOException { if (_htmClient == null) { Log.w(TAG, "Not connected to any server yet"); return 0; } // Check for connectivity if (!_htmClient.isOnline()) { return 0; } // Get metrics from server List<Metric> remoteMetrics = _htmClient.getMetrics(); if (remoteMetrics == null) { Log.e(TAG, "Unable to load metrics from server. " + _htmClient.getServerUrl()); return 0; } int newMetrics = 0; HashSet<String> metricSet = new HashSet<String>(); // Save results to database boolean dataChanged = false; Metric localMetric; CoreDatabase database = HTMApplication.getDatabase(); for (Metric remoteMetric : remoteMetrics) { // Check if it is a new metric localMetric = database.getMetric(remoteMetric.getId()); if (localMetric == null) { database.addMetric(remoteMetric); dataChanged = true; newMetrics++; } else { // Check for metric changes if (remoteMetric.getLastRowId() != localMetric.getLastRowId()) { // Use local metric last timestamp remoteMetric.setLastTimestamp(localMetric.getLastTimestamp()); // Update metric. database.updateMetric(remoteMetric); } } metricSet.add(remoteMetric.getId()); } // Consolidate database by removing metrics from local cache // that were removed from the server try { for (Metric metric : database.getAllMetrics()) { if (!metricSet.contains(metric.getId())) { database.deleteMetric(metric.getId()); dataChanged = true; } } } catch (Exception e) { Log.e(TAG, "Error loading metrics", e); } finally { // Notify receivers new data has arrived if (dataChanged) { fireMetricChangedEvent(); } } return newMetrics; } /** * Schedule the update task to execute periodically at the given rate * * @param rate The rate given in minutes */ protected synchronized void scheduleUpdate(long rate) { if (_updateTask != null) { _updateTask.cancel(true); } _updateTask = _service.scheduleTask(new Runnable() { @Override public void run() { try { synchronizeWithServer(); } catch (Exception e) { Log.e(TAG, "Error updating data", e); } } }, rate, TimeUnit.MINUTES); } /** * This method is execute periodically and update {@link com.numenta.core.data.CoreDatabase} * with new data from the * server. */ protected void synchronizeWithServer() throws IOException { Log.i(TAG, "synchronizeWithServer"); if (_synchronizingWithServer) { return; } if (!NetUtils.isConnected()) { // Not connected, skip until we connect return; } final CoreDatabase database = HTMApplication.getDatabase(); if (database == null) { return; } synchronized (this) { if (_synchronizingWithServer) { return; } _synchronizingWithServer = true; } String result = null; try { // Guard against blocking the UI Thread if (Looper.myLooper() == Looper.getMainLooper()) { throw new IllegalStateException("You should not access the database from the UI thread"); } fireRefreshStateEvent(_synchronizingWithServer); final Context context = _service.getApplicationContext(); final long now = System.currentTimeMillis(); // Check if enough time has passed since we checked for new data SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final long lastConnectedTime = prefs.getLong(PREF_LAST_CONNECTED_TIME, 0); if (now - lastConnectedTime < DataUtils.METRIC_DATA_INTERVAL) { return; } // Calculate hours since last update. This information will be // passed to the user together with error message final CharSequence hoursSinceData = DateUtils.getRelativeTimeSpanString(database.getLastTimestamp(), now, DateUtils.MINUTE_IN_MILLIS); Future<?> pendingIO = null; try { // Try to connect to server if (_htmClient == null) { _htmClient = _service.connectToServer(); } if (_htmClient == null) { throw new IOException("Unable to connect to server"); } // Update last connected time SharedPreferences.Editor editor = prefs.edit(); editor.putLong(PREF_LAST_CONNECTED_TIME, now); editor.apply(); // Start by downloading all the metrics available from backend // in a background IO thread pendingIO = _service.getIOThreadPool().submit(new Callable<Void>() { @Override public Void call() throws Exception { try { // First load metrics loadAllMetrics(); // Load all annotations after metrics loadAllAnnotations(); // Load all data after annotations loadAllData(); // Synchronize notifications after data synchronizeNotifications(); // Synchronize application data last HTMApplication.getInstance().loadApplicationData(_htmClient); } catch (android.database.sqlite.SQLiteFullException e) { // Try to delete old records to make room if possible Log.e(TAG, "Failed to save data into database", e); database.deleteOldRecords(); } return null; } }); // Wait for metric data to finish pendingIO.get(); } catch (InterruptedException e) { // Cancel pending tasks if (!pendingIO.isDone()) { pendingIO.cancel(true); } Log.w(TAG, "Interrupted while loading data"); } catch (ExecutionException e) { // Cancel pending tasks if (!pendingIO.isDone()) { pendingIO.cancel(true); } Throwable original = e.getCause(); if (original instanceof AuthenticationException) { _service.fireAuthenticationFailedEvent(); } else if (original instanceof ObjectNotFoundException) { Log.e(TAG, "Error loading data", e); result = context.getString(R.string.refresh_update_error, hoursSinceData); } else if (original instanceof IOException) { Log.e(TAG, "Unable to connect", e); result = context.getString(R.string.refresh_server_unreachable, hoursSinceData); } else { Log.e(TAG, "Error loading data", e); result = context.getString(R.string.refresh_update_error, hoursSinceData); } } catch (AuthenticationException e) { _service.fireAuthenticationFailedEvent(); } catch (HTMException e) { Log.e(TAG, "Error loading data", e); result = context.getString(R.string.refresh_update_error, hoursSinceData); } catch (IOException e) { Log.e(TAG, "Unable to connect", e); result = context.getString(R.string.refresh_server_unreachable, hoursSinceData); } } finally { _synchronizingWithServer = false; fireRefreshStateEvent(_synchronizingWithServer, result); } } /** * Called periodically to synchronize notifications in the background */ protected void synchronizeNotifications() { try { _service.synchronizeNotifications(); } catch (HTMException e) { Log.e(TAG, "Failed to synchronize notifications", e); } catch (IOException e) { Log.e(TAG, "Failed to synchronize notifications", e); } } /** * Loads data for all metrics asynchronous. */ protected void loadAllData() throws HTMException, IOException { } /** * Load all annotations from server and update the local database by adding new annotations and * removing old ones. This method will fire {@link DataSyncService#ANNOTATION_CHANGED_EVENT} * <p><b>Note:</b></p> * This method will only load the last {@link HTMApplication#getNumberOfDaysToSync()} * days of data. */ protected void loadAllAnnotations() throws IOException, HTMException { if (_htmClient == null) { Log.w(TAG, "Not connected to any server yet"); return; } // Get Annotations from server for the last 2 weeks long now = System.currentTimeMillis(); long from = now - HTMApplication.getNumberOfDaysToSync() * DataUtils.MILLIS_PER_DAY; List<Annotation> remoteAnnotations = _htmClient.getAnnotations(new Date(from), new Date(now)); if (remoteAnnotations == null) { Log.e(TAG, "Unable to load annotations from server. " + _htmClient.getServerUrl()); return; } HashSet<String> activeAnnotations = new HashSet<String>(); HashSet<String> localAnnotations = new HashSet<String>(); // Save results to database boolean dataChanged = false; CoreDatabase database = HTMApplication.getDatabase(); // Get a set of all annotations in the database for (Annotation annotation : database.getAllAnnotations()) { localAnnotations.add(annotation.getId()); } for (Annotation remote : remoteAnnotations) { // Check if it is a new annotation if (!localAnnotations.contains(remote.getId())) { // Add annotation to database database.addAnnotation(remote); dataChanged = true; } activeAnnotations.add(remote.getId()); } // Consolidate database by removing annotations from local database // that were removed from the server try { for (String annotation : localAnnotations) { if (!activeAnnotations.contains(annotation)) { // delete annotation database.deleteAnnotation(annotation); dataChanged = true; } } } catch (Exception e) { Log.e(TAG, "Error loading annotations", e); } finally { // Notify receivers of changes if (dataChanged) { fireAnnotationChangedEvent(); } } } /** * Delete annotation from the server * * @param annotationId The annotation ID to delete * @return {@code true} if the annotation was successfully deleted from the server */ protected boolean deleteAnnotation(String annotationId) { if (_htmClient == null) { Log.w(TAG, "Not connected to any server yet"); return false; } try { CoreDatabase database = HTMApplication.getDatabase(); // Check if annotation exists Annotation annotation = database.getAnnotation(annotationId); if (annotation != null) { // Delete from the server _htmClient.deleteAnnotation(annotation); // Delete from the database if (database.deleteAnnotation(annotationId) == 1) { // Notify receivers of changes fireAnnotationChangedEvent(); return true; } } // Annotation not found Log.e(TAG, "Failed to delete annotation. " + annotationId + " was not found"); } catch (HTMException e) { Log.e(TAG, "Failed to delete annotation " + annotationId, e); } catch (IOException e) { Log.e(TAG, "Failed to delete annotation " + annotationId, e); } return false; } /** * Add new annotation associating it to the given server and the given timestamp. * The current device will also be associated with the annotation. * * @param timestamp The date and time to be annotated * @param server Instance Id associated with this annotation * @param message Annotation message * @param user User name * @return {@code true} if the annotation was successfully added to the server */ public boolean addAnnotation(Date timestamp, String server, String message, String user) { if (_htmClient == null) { Log.w(TAG, "Not connected to any server yet"); return false; } try { Annotation annotation = _htmClient.addAnnotation(timestamp, server, message, user); // Update database with new annotation if (annotation != null) { CoreDatabase database = HTMApplication.getDatabase(); if (database.addAnnotation(annotation) != -1) { fireAnnotationChangedEvent(); return true; } } } catch (HTMException e) { Log.e(TAG, "Failed to add annotation ", e); } catch (IOException e) { Log.e(TAG, "Failed to add annotation ", e); } return false; } /** * Force client to refresh the data by downloading new data from the server */ protected void forceRefresh() { Log.i(TAG, "forceRefresh"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_service.getApplicationContext()); prefs.registerOnSharedPreferenceChangeListener(_preferenceChangeListener); scheduleUpdate(Long.parseLong(prefs.getString(PREF_DATA_REFRESH_RATE, REFRESH_RATE))); } /** * Start the data sync service. * <p> * Should only be called by {@link DataService} * </p> */ protected void start() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_service.getApplicationContext()); prefs.registerOnSharedPreferenceChangeListener(_preferenceChangeListener); scheduleUpdate(Long.parseLong(prefs.getString(PREF_DATA_REFRESH_RATE, REFRESH_RATE))); } /** * Stop the data sync service. * <p> * Should only be called by {@link DataService} * </p> */ protected void stop() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(_service.getApplicationContext()); prefs.unregisterOnSharedPreferenceChangeListener(_preferenceChangeListener); if (_updateTask != null) { _updateTask.cancel(true); } _updateTask = null; } /** * Returns {@code true} if the service is refreshing the data */ public boolean isRefreshing() { return _synchronizingWithServer; } /** * Returns API Client */ protected HTMClient getClient() { return _htmClient; } /** * Return underlying background service */ public DataService getService() { return _service; } /** * Close server connection */ public void closeConnection() { _htmClient = null; } }