com.numenta.htmit.mobile.instance.InstanceListFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.numenta.htmit.mobile.instance.InstanceListFragment.java

Source

/*
 * 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.htmit.mobile.instance;

import com.numenta.htmit.mobile.HTMITApplication;
import com.numenta.htmit.mobile.R;
import com.numenta.htmit.mobile.SortOrder;
import com.numenta.htmit.mobile.annotation.AddAnnotationActivity;
import com.numenta.htmit.mobile.annotation.AnnotationListActivity;
import com.numenta.htmit.mobile.service.HTMITClientImpl;
import com.numenta.core.data.AggregationType;
import com.numenta.core.service.DataSyncService;
import com.numenta.core.ui.chart.AnomalyChartView;
import com.numenta.core.utils.Log;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.content.LocalBroadcastManager;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * This Fragment is responsible for displaying the server list. The list item is
 * composed of the server name, notification icon and combined anomaly chart
 * aggregated by Hour, Day or Week.
 */
public class InstanceListFragment extends ListFragment {

    private static final String TAG = InstanceListFragment.class.getSimpleName();
    private InstanceListAdapter _listAdapter;
    private volatile SortOrder _sortOrder;
    private volatile boolean _sorting;
    private AggregationType _aggregation;

    // Event listeners
    private final BroadcastReceiver _metricDataChangedReceiver;
    private final BroadcastReceiver _metricChangedReceiver;
    private final BroadcastReceiver _annotationChangedReceiver;

    // Check if sort selection changed
    private final PropertyChangeListener _sortSelectionChangeListener = new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent event) {
            sort(false);
        }
    };

    private AsyncTask<Void, InstanceAnomalyChartData, Void> _metricLoadTask;

    /**
     * InstanceListFragment constructor
     */
    public InstanceListFragment() {
        // Listen for new metric data
        _metricDataChangedReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateMetricData();
            }
        };
        // Listens for new metrics
        _metricChangedReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateInstanceList();
            }
        };

        // Listen for annotations
        _annotationChangedReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateMetricData();
            }
        };
    }

    /**
     * Refresh {@link ListAdapter} with new metric data if available
     */
    private void updateMetricData() {
        if (_listAdapter != null) {
            // Check if we are already updating the metric data from a previous
            // call
            if (_metricLoadTask != null && _metricLoadTask.getStatus() != AsyncTask.Status.FINISHED) {
                return;
            }
            // Background task for loading metric data
            _metricLoadTask = new AsyncTask<Void, InstanceAnomalyChartData, Void>() {
                @Override
                protected void onPostExecute(Void servers) {
                    if (isCancelled())
                        return;
                    // Force sort once done
                    sort(true);
                }

                @Override
                protected Void doInBackground(Void... params) {
                    // Get current list of metrics kept in the adapter.
                    ArrayList<InstanceAnomalyChartData> instances = new ArrayList<InstanceAnomalyChartData>();
                    synchronized (_listAdapter) {
                        for (int i = 0, count = _listAdapter.getCount(); i < count; i++) {
                            if (isCancelled())
                                return null;
                            instances.add(_listAdapter.getItem(i));
                        }
                    }
                    for (InstanceAnomalyChartData data : instances) {
                        if (isCancelled())
                            return null;
                        // Refresh the content of each metric and resort if the
                        // data was changed as we load the rest of the data.
                        // This partial resort is done in order to give the user
                        // some feedback as the rest of the data loads.
                        data.setEndDate(null);
                        if (data.load()) {
                            publishProgress(data);
                        }
                    }
                    return null;
                }

                @Override
                protected void onProgressUpdate(InstanceAnomalyChartData... data) {
                    if (isCancelled())
                        return;
                    // Partial resort
                    sort(true);
                }
            }.execute();
        }
    }

    /**
     * Sort the contents of the list based on {@link HTMITApplication#getSort()}
     *
     * @param force {@code true} to force the contents to be sorted, otherwise
     *              the contents will only sort if the current sort order is
     *              different than {@link HTMITApplication#getSort()}
     */
    private void sort(boolean force) {
        if (_sorting) {
            return;
        }
        synchronized (this) {
            if (_sorting) {
                return;
            }
            _sorting = true;
        }
        try {
            if (_listAdapter != null && !_listAdapter.isEmpty()
                    && (force || _sortOrder != HTMITApplication.getSort())) {
                _sortOrder = HTMITApplication.getSort();
                synchronized (_listAdapter) {
                    switch (_sortOrder) {
                    case Name:
                        _listAdapter.sort(InstanceAnomalyChartData.SORT_BY_NAME);
                        break;
                    default:
                        _listAdapter.sort(InstanceAnomalyChartData.SORT_BY_ANOMALY);
                        break;
                    }
                }
                _listAdapter.notifyDataSetChanged();
            }
        } finally {
            _sorting = false;
        }
    }

    /**
     * Refresh Instance List with contents from the database
     */
    private void updateInstanceList() {
        if (_listAdapter != null && _aggregation != null) {
            new AsyncTask<Void, Void, Set<String>>() {

                @Override
                protected Set<String> doInBackground(Void... params) {
                    return HTMITApplication.getDatabase().getAllInstances();
                }

                @Override
                protected void onPostExecute(Set<String> servers) {
                    if (isCancelled()) {
                        return;
                    }
                    boolean updated = false;
                    HashSet<String> existing = new HashSet<String>();
                    HashSet<InstanceAnomalyChartData> deleted = new HashSet<InstanceAnomalyChartData>();
                    // synchronized (_listAdapter) {
                    _listAdapter.setNotifyOnChange(false);
                    try {
                        synchronized (_listAdapter) {

                            // Remove deleted instances
                            for (int i = 0, count = _listAdapter.getCount(); i < count; i++) {
                                if (isCancelled()) {
                                    return;
                                }
                                InstanceAnomalyChartData instance = _listAdapter.getItem(i);
                                if (servers.contains(instance.getId())) {
                                    existing.add(instance.getId());
                                } else {
                                    deleted.add(instance);
                                    updated = true;
                                }
                            }

                            // Remove deleted instances
                            if (isCancelled()) {
                                return;
                            }
                            for (InstanceAnomalyChartData instance : deleted) {
                                if (isCancelled()) {
                                    return;
                                }
                                _listAdapter.remove(instance);
                                updated = true;
                            }

                            // Add new instances
                            if (isCancelled()) {
                                return;
                            }
                            for (String newServer : servers) {
                                if (isCancelled()) {
                                    return;
                                }
                                if (existing.contains(newServer)) {
                                    continue;
                                }
                                _listAdapter.add(new InstanceAnomalyChartData(newServer, _aggregation));
                                updated = true;
                            }
                        }
                        if (isCancelled()) {
                            return;
                        }

                        if (updated) {
                            if (getListAdapter() == null) {
                                setListAdapter(_listAdapter);
                            }

                            updateMetricData();
                            _listAdapter.notifyDataSetChanged();
                        }
                    } finally {
                        _listAdapter.setNotifyOnChange(true);
                        _listAdapter.sort(InstanceListAdapter.getComparator());
                    }
                }

                @Override
                protected void onCancelled() {
                    _listAdapter.notifyDataSetChanged();
                }
            }.execute();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (_listAdapter != null) {
            _listAdapter.setNotifyOnChange(false);
        }

        HTMITApplication.removePropertyChangeListener(HTMITApplication.SORT_PROPERTY, _sortSelectionChangeListener);
        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(_metricChangedReceiver);
        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(_annotationChangedReceiver);
        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(_metricDataChangedReceiver);
    }

    @Override
    public void onResume() {
        super.onResume();
        updateInstanceList();
        updateMetricData();
        if (_listAdapter != null) {
            _listAdapter.setNotifyOnChange(true);
            sort(false);
        }

        // Attach event listeners
        HTMITApplication.addPropertyChangeListener(HTMITApplication.SORT_PROPERTY, _sortSelectionChangeListener);

        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(_metricChangedReceiver,
                new IntentFilter(DataSyncService.METRIC_CHANGED_EVENT));
        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(_metricDataChangedReceiver,
                new IntentFilter(DataSyncService.METRIC_DATA_CHANGED_EVENT));
        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(_annotationChangedReceiver,
                new IntentFilter(DataSyncService.ANNOTATION_CHANGED_EVENT));
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Create server list adapter and refresh its contents from the database
        _listAdapter = new InstanceListAdapter(activity, new ArrayList<InstanceAnomalyChartData>());
        updateInstanceList();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // On long click the list fragment will open the context menu.
        // We use this event to add our "Add Annotation" option.
        registerForContextMenu(getListView());
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, view, menuInfo);
        //TODO:FEATURE_FLAG: Annotations were introduced in version 1.6
        if (HTMITApplication.getInstance().getServerVersion().compareTo(HTMITClientImpl.SERVER_1_6) < 0) {
            return;
        }
        if (isMenuVisible()) {
            // Make sure we have data for this instance before showing the menu
            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
            InstanceAnomalyChartData instance = (InstanceAnomalyChartData) getListAdapter().getItem(info.position);
            if (instance != null && instance.hasData()) {
                // Get timestamp from the selected bar on the anomaly chart
                AnomalyChartView targetView = (AnomalyChartView) info.targetView
                        .findViewById(R.id.anomaly_chart_view);
                long selectedTimestamp = targetView.getSelectedTimestamp();
                if (selectedTimestamp == -1) {
                    // The user did not select any anomaly bar. He must have clicked around the chart
                    return;
                }
                menu.add(0, R.id.menu_add_annotation, 0, R.string.menu_add_annotation);
            }
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (isMenuVisible() && item.getItemId() == R.id.menu_add_annotation) {
            // Get instance from context menu position
            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
            InstanceAnomalyChartData instance = (InstanceAnomalyChartData) getListAdapter().getItem(info.position);

            // Get timestamp from the selected bar on the anomaly chart
            AnomalyChartView view = (AnomalyChartView) info.targetView.findViewById(R.id.anomaly_chart_view);
            long selectedTimestamp = view.getSelectedTimestamp();
            if (selectedTimestamp == -1) {
                // Should not happen
                Log.w(TAG, "Failed to get annotation timestamp from chart view. Using current time instead");
                return true;
            }

            // Open "Add  Annotation" activity
            Intent addAnnotation = new Intent(getActivity(), AddAnnotationActivity.class);
            addAnnotation.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_TOP
                    | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            addAnnotation.putExtra(AddAnnotationActivity.EXTRA_INSTANCE_ID, instance.getId());
            addAnnotation.putExtra(AddAnnotationActivity.EXTRA_TIMESTAMP, selectedTimestamp);
            getActivity().startActivity(addAnnotation);
        }

        return super.onContextItemSelected(item);
    }

    @Override
    public void onListItemClick(ListView list, View item, int position, long id) {

        // Get item from position
        InstanceAnomalyChartData instance = (InstanceAnomalyChartData) getListAdapter().getItem(position);

        // Make user the instance has data before opening the detail page
        if (instance == null || !instance.hasData()) {
            // Nothing to show
            return;
        }

        // Check if clicked on annotations
        // If bar has annotations on the selected timestamp then open the annotation list
        AnomalyChartView view = (AnomalyChartView) item.findViewById(R.id.anomaly_chart_view);
        long timestamp = view.getSelectedTimestamp();
        long selectedTimestamp = -1;

        // The bar selection will be flexible, accepting user click on the bar with annotation
        // or any neighbouring bars.
        if (instance.hasAnnotationsForTime(timestamp)) {
            // The user clicked exactly on the bar with annotation
            selectedTimestamp = timestamp;
        } else if (instance.hasAnnotationsForTime(timestamp - instance.getAggregation().milliseconds())) {
            // The user clicked on the previous bar
            selectedTimestamp = timestamp - instance.getAggregation().milliseconds();
        } else if (instance.hasAnnotationsForTime(timestamp + instance.getAggregation().milliseconds())) {
            // The user clicked on the next bar
            selectedTimestamp = timestamp + instance.getAggregation().milliseconds();
        }

        if (selectedTimestamp != -1) {
            // Open annotation list
            Intent openListIntent = new Intent(getActivity(), AnnotationListActivity.class);
            openListIntent.putExtra(AnnotationListActivity.EXTRA_INSTANCE_DATA, instance);
            openListIntent.putExtra(AnnotationListActivity.EXTRA_TIMESTAMP, selectedTimestamp);
            getActivity().startActivity(openListIntent);
            return;
        }

        // Show detail page
        Intent instanceDetail = new Intent(getActivity(), InstanceDetailActivity.class);
        instanceDetail.putExtra(InstanceDetailActivity.INSTANCE_ID_ARG, instance.getId());
        startActivity(instanceDetail);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Bundle args = getArguments();
        if (args != null) {
            _aggregation = (AggregationType) args.get(AggregationType.class.getCanonicalName());
            if (_listAdapter != null) {
                _listAdapter.setAggregation(_aggregation);
                updateInstanceList();
            }
        }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            updateInstanceList();
        } else if (isAdded()) {
            setSelection(0);
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (_metricLoadTask != null) {
            _metricLoadTask.cancel(true);
        }
    }
}