com.numenta.taurus.instance.InstanceListFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.numenta.taurus.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.taurus.instance;

import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;

import com.numenta.core.data.AggregationType;
import com.numenta.core.service.DataSyncService;
import com.numenta.core.utils.DataUtils;
import com.numenta.taurus.R;
import com.numenta.taurus.TaurusApplication;
import com.numenta.taurus.TaurusBaseActivity;
import com.numenta.taurus.chart.TimeSliderView;
import com.numenta.taurus.service.TaurusDataSyncService;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ListFragment;
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.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.GestureDetectorCompat;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Filter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Toast;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Date;
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 and anomaly chart
 */
public class InstanceListFragment extends ListFragment {

    private static final String TAG = InstanceListFragment.class.getSimpleName();

    private InstanceListAdapter _listAdapter;

    private final AggregationType _aggregation = TaurusApplication.getAggregation();

    // Event listeners
    private final BroadcastReceiver _instanceDataChangedReceiver;

    private final PropertyChangeListener _favoritesChangedReceiver;

    private AsyncTask<Void, Void, Boolean> _instanceLoadTask;

    private InstanceFilter _instanceFilter = InstanceFilter.None;

    private TimeSliderView _timeView;

    private boolean _scrolling;

    // Whether or not the list view is currently loading  data
    volatile boolean _loading;

    /** Horizontal scrolling threshold angle (30 deg) */
    static double HORIZ_SCROLLING_THRESHOLD_ANGLE = Math.PI / 6;

    private Toast _toast;

    /**
     * Handles date/time scrolling
     */
    final class GestureListener implements GestureDetector.OnGestureListener {

        private final View _view;

        long _initialTimestamp;

        public GestureListener(View view) {
            this._view = view;
        }

        @Override
        public boolean onDown(MotionEvent e) {
            Date endDate = _listAdapter.getEndDate();
            _initialTimestamp = endDate == null ? TaurusApplication.getDatabase().getLastTimestamp()
                    : endDate.getTime();
            _scrolling = false;
            return false;
        }

        @Override
        public void onShowPress(MotionEvent e) {
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            long distance = getDistance(e1, e2);
            if (Math.abs(distance) > 1) {
                // Check if scrolling on a 45 degree angle
                double angle = Math.atan2(e1.getY() - e2.getY(), e1.getX() - e2.getX());
                if (_scrolling
                        || angle >= -HORIZ_SCROLLING_THRESHOLD_ANGLE && angle <= HORIZ_SCROLLING_THRESHOLD_ANGLE
                        || angle >= Math.PI - HORIZ_SCROLLING_THRESHOLD_ANGLE
                        || angle <= -Math.PI + HORIZ_SCROLLING_THRESHOLD_ANGLE) {
                    Date scrolledDate = new Date(DataUtils.floorTo5minutes(_initialTimestamp + distance));
                    if (_listAdapter != null) {
                        _listAdapter.setEndDate(scrolledDate);
                        scrolledDate = _listAdapter.getEndDate();
                        _timeView.setEndDate(scrolledDate.getTime());
                    }
                    // Show current scrolled date
                    _toast.setText(String.format("%1$tA, %1$tb %1$te", scrolledDate));
                    _toast.show();
                    _scrolling = true;
                }
            }
            return _scrolling;
        }

        /**
         * Calculate the time interval between 2 events
         *
         * @return Time interval in milliseconds
         */
        private long getDistance(MotionEvent e1, MotionEvent e2) {
            final float x1 = e1.getX();
            final float x2 = e2.getX();
            final float distance = x1 - x2;
            int width = _view.getMeasuredWidth();
            int pixels = width / TaurusApplication.getTotalBarsOnChart();
            int scrolledBars = (int) (distance / pixels);
            // Scroll date by aggregation interval
            long interval = _aggregation.milliseconds();
            return interval * scrolledBars;
        }

        @Override
        public void onLongPress(MotionEvent e) {
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    }

    final class TouchListener implements View.OnTouchListener {

        // Attach gesture detector to values chart handling scrolling.
        final GestureDetectorCompat _gestureDetector;

        // The current view
        final View _view;

        public TouchListener(View view) {
            _view = view;
            _gestureDetector = new GestureDetectorCompat(getActivity(), new GestureListener(_view));
        }

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                // Fall through
            case MotionEvent.ACTION_CANCEL:
                // Done scrolling, refresh the chart scale
                v.post(new Runnable() {
                    @Override
                    public void run() {
                        // Hide text if scrolled back 2 hours
                        long threshold = TaurusApplication.getDatabase().getLastTimestamp()
                                - DataUtils.MILLIS_PER_HOUR * 2;
                        Date endDate = _listAdapter.getEndDate();
                        if (endDate != null && endDate.getTime() < threshold) {
                            _listAdapter.showHeaderText(false);
                        } else {
                            _listAdapter.showHeaderText(true);
                        }
                    }
                });
                break;
            default:
                break;
            }

            // Detect scroll gestures on the metric detail chart
            return _gestureDetector.onTouchEvent(event);
        }
    }

    /**
     * InstanceListFragment constructor
     */
    public InstanceListFragment() {
        // Listen for new instance data
        _instanceDataChangedReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateInstanceData();
            }
        };

        // Listen for favorites changes
        _favoritesChangedReceiver = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent event) {
                if (_instanceFilter == InstanceFilter.Favorites) {
                    filterFavorites();
                } else {
                    clearFilter();
                }
            }
        };
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_instance_list, container, false);
        _timeView = (TimeSliderView) view.findViewById(R.id.time_slider);
        _timeView.setCollapsed(false);
        setListAdapter(_listAdapter);
        return view;
    }

    @SuppressLint("ShowToast")
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        View list = getListView();
        list.setOnTouchListener(new TouchListener(list));
        _toast = Toast.makeText(getActivity(), "", Toast.LENGTH_SHORT);
    }

    /**
     * Refresh {@link ListAdapter} with new instance data if available
     */
    private void updateInstanceData() {
        if (_loading) {
            return;
        }
        synchronized (this) {
            // Cancel previous task
            if (_instanceLoadTask != null) {
                cancelTrackedBackgroundTask(_instanceLoadTask);
            }
            _loading = true;
        }
        if (_listAdapter != null) {
            // Background task for loading instance data
            _instanceLoadTask = new AsyncTask<Void, Void, Boolean>() {
                HashSet<InstanceAnomalyChartData> deletedItems = new HashSet<InstanceAnomalyChartData>();

                HashSet<InstanceAnomalyChartData> addedItems = new HashSet<InstanceAnomalyChartData>();

                @Override
                protected void onPostExecute(Boolean modified) {
                    if (isCancelled()) {
                        return;
                    }
                    // Add new instances
                    if (!addedItems.isEmpty()) {
                        _listAdapter.addAll(addedItems.toArray(new InstanceAnomalyChartData[addedItems.size()]));
                    }

                    // Remove deleted instances
                    for (InstanceAnomalyChartData item : deletedItems) {
                        _listAdapter.remove(item);
                    }
                    // Refresh
                    _listAdapter.sort();
                    updateTimeSlider();

                    if (_instanceFilter == InstanceFilter.Favorites) {
                        filterFavorites();
                    }
                    _loading = false;
                }

                @SuppressWarnings("SynchronizeOnNonFinalField")
                @Override
                protected Boolean doInBackground(Void... params) {
                    if (isCancelled()) {
                        return false;
                    }
                    // Consolidate adapter instance list with database
                    Set<String> persistedInstances = TaurusApplication.getDatabase().getAllInstances();
                    ArrayList<InstanceAnomalyChartData> items = new ArrayList<InstanceAnomalyChartData>();
                    HashSet<String> adapterInstances = new HashSet<String>();
                    boolean modified = false;
                    Date endDate;

                    synchronized (_listAdapter) {

                        endDate = _listAdapter.getEndDate();
                        // Find deleted instances
                        for (InstanceAnomalyChartData item : _listAdapter.getUnfilteredItems()) {
                            if (isCancelled()) {
                                return false;
                            }
                            if (persistedInstances.contains(item.getId())) {
                                adapterInstances.add(item.getId());
                                items.add(item);
                            } else {
                                modified = true;
                                deletedItems.add(item);
                            }
                        }
                    }

                    // Find new instances
                    if (isCancelled()) {
                        return false;
                    }
                    for (String instance : persistedInstances) {
                        if (isCancelled()) {
                            return false;
                        }
                        if (adapterInstances.contains(instance)) {
                            continue;
                        }
                        addedItems.add(new InstanceAnomalyChartData(instance, _aggregation));
                        modified = true;
                    }

                    // Refresh current items  data.
                    for (InstanceAnomalyChartData item : items) {
                        if (isCancelled()) {
                            return null;
                        }
                        item.setEndDate(endDate);
                        modified = item.load() || modified;
                    }

                    // Load data for the new instances.
                    for (InstanceAnomalyChartData item : addedItems) {
                        if (isCancelled()) {
                            return null;
                        }
                        item.setEndDate(endDate);
                        modified = item.load() || modified;
                    }
                    return modified;
                }

                @Override
                protected void onCancelled() {
                    _listAdapter.sort();
                    _listAdapter.notifyDataSetChanged();
                    _loading = false;
                }

            }.executeOnExecutor(TaurusApplication.getWorkerThreadPool());
            trackBackgroundTask(_instanceLoadTask);
        }
    }

    private void trackBackgroundTask(AsyncTask task) {
        Activity activity = getActivity();
        if (activity instanceof TaurusBaseActivity) {
            ((TaurusBaseActivity) activity).trackBackgroundTask(task);
        }
    }

    private void cancelTrackedBackgroundTask(AsyncTask task) {
        Activity activity = getActivity();
        if (activity instanceof TaurusBaseActivity) {
            ((TaurusBaseActivity) activity).cancelTrackedBackgroundTask(task);
        }
    }

    /**
     * Apply "favorite" instance filter to the list
     *
     * @see com.numenta.taurus.TaurusApplication#isInstanceFavorite(String)
     */
    public void filterFavorites() {
        _instanceFilter = InstanceFilter.Favorites;
        if (_listAdapter != null) {
            Filter listFilter = _listAdapter.getFavoritesFilter();
            if (listFilter != null) {
                listFilter.filter(null);
            }
        }
    }

    public void applyFilter(CharSequence query) {
        if (_listAdapter != null && query != null) {
            Filter listFilter = _listAdapter.getFilter();
            if (listFilter != null) {
                listFilter.filter(query);
            }
        }
    }

    /**
     * Remove any filter and show all instances
     */
    public void clearFilter() {
        _instanceFilter = InstanceFilter.None;
        if (_listAdapter != null) {
            _listAdapter.clearFilter();
        }
    }

    /**
     * Whether or not show group header
     */
    public void showHeaders(boolean show) {
        if (_listAdapter != null) {
            _listAdapter.showHeaders(show);
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(_instanceDataChangedReceiver);
        TaurusApplication.removePropertyChangeListener(TaurusApplication.FAVORITE_PROPERTY,
                _favoritesChangedReceiver);
    }

    @Override
    public void onResume() {
        super.onResume();
        updateInstanceData();

        // Attach event listeners
        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(_instanceDataChangedReceiver,
                new IntentFilter(TaurusDataSyncService.INSTANCE_DATA_CHANGED_EVENT));
        LocalBroadcastManager.getInstance(getActivity()).registerReceiver(_instanceDataChangedReceiver,
                new IntentFilter(DataSyncService.METRIC_CHANGED_EVENT));
        TaurusApplication.addPropertyChangeListener(TaurusApplication.FAVORITE_PROPERTY, _favoritesChangedReceiver);

    }

    /**
     * Update {@link com.numenta.taurus.chart.TimeSliderView} matching the dates of this fragment's
     * list adapter.
     *
     * @see com.numenta.taurus.R.id#time_slider
     * @see com.numenta.taurus.R.layout#fragment_instance_list
     */
    private void updateTimeSlider() {
        // Update time
        Long end = _listAdapter.getEndDate() != null ? _listAdapter.getEndDate().getTime()
                : TaurusApplication.getDatabase().getLastTimestamp();
        View mainView = getView();
        if (mainView != null) {
            _timeView.setAggregation(_aggregation);
            _timeView.setEndDate(end);
        }
    }

    @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>());
        _listAdapter.setAggregation(_aggregation);
    }

    @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) {
        if (_scrolling) {
            return;
        }
        super.onCreateContextMenu(menu, view, menuInfo);
        // Make sure we have an instance before showing the menu
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        InstanceAnomalyChartData instance = (InstanceAnomalyChartData) getListAdapter().getItem(info.position);
        if (instance != null) {
            // Show Context menu with "Add/Remove Favorites" depending on the instance selected
            if (!TaurusApplication.isInstanceFavorite(instance.getId())) {
                menu.add(0, R.id.menu_add_favorite, 0, R.string.menu_add_favorite);
            } else {
                menu.add(0, R.id.menu_remove_favorite, 0, R.string.menu_remove_favorite);
            }
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        // Get instance from context menu position
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        InstanceAnomalyChartData instance = (InstanceAnomalyChartData) getListAdapter().getItem(info.position);
        Tracker t = TaurusApplication.getInstance().getGoogleAnalyticsTracker();

        switch (item.getItemId()) {
        case R.id.menu_add_favorite:
            TaurusApplication.addInstanceToFavorites(instance.getId());
            t.send(new HitBuilders.EventBuilder().setCategory("Favorites").setAction("Add")
                    .setLabel(instance.getTicker()).build());
            return true;
        case R.id.menu_remove_favorite:
            TaurusApplication.removeInstanceFromFavorites(instance.getId());
            t.send(new HitBuilders.EventBuilder().setCategory("Favorites").setAction("Remove")
                    .setLabel(instance.getTicker()).build());
            return true;
        }
        return super.onContextItemSelected(item);

    }

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

        // Check for network connection before opening detail screen
        Activity activity = getActivity();
        if (activity instanceof TaurusBaseActivity && !((TaurusBaseActivity) activity).checkNetworkConnection()) {
            return;
        }

        // 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;
        }

        // Show detail page
        Intent instanceDetail = new Intent(getActivity(), InstanceDetailActivity.class);
        instanceDetail.putExtra(InstanceDetailActivity.INSTANCE_ID_ARG, instance.getId());
        instanceDetail.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        Date endDate = instance.getEndDate();
        instanceDetail.putExtra(InstanceDetailActivity.TIMESTAMP_ARG,
                endDate == null ? 0 : instance.getEndDate().getTime());
        startActivity(instanceDetail);
    }

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