com.meiste.tempalarm.ui.CurrentTemp.java Source code

Java tutorial

Introduction

Here is the source code for com.meiste.tempalarm.ui.CurrentTemp.java

Source

/*
 * Copyright (C) 2014 Gregory S. Meiste  <http://gregmeiste.com>
 *
 * 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.meiste.tempalarm.ui;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Dialog;
import android.app.LoaderManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.SyncStatusObserver;
import android.database.Cursor;
import android.preference.PreferenceManager;
import android.support.v4.widget.SimpleCursorAdapter;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.jjoe64.graphview.CustomLabelFormatter;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.GraphViewSeries;
import com.jjoe64.graphview.LineGraphView;
import com.meiste.greg.gcm.GCMHelper;
import com.meiste.tempalarm.AppConstants;
import com.meiste.tempalarm.R;
import com.meiste.tempalarm.backend.registration.Registration;
import com.meiste.tempalarm.provider.RasPiContract;
import com.meiste.tempalarm.sync.AccountUtils;
import com.meiste.tempalarm.sync.SyncAdapter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import butterknife.ButterKnife;
import butterknife.InjectView;
import timber.log.Timber;

public class CurrentTemp extends ActionBarActivity
        implements GCMHelper.OnGcmRegistrationListener, LoaderManager.LoaderCallbacks<Cursor> {

    private static final int GPS_REQUEST = 1337;
    private static final int ACCOUNT_PICKER_REQUEST = 1338;

    /**
     * Projection for querying the content provider.
     */
    private static final String[] PROJECTION = new String[] { RasPiContract.RasPiReport._ID,
            RasPiContract.RasPiReport.COLUMN_NAME_TIMESTAMP, RasPiContract.RasPiReport.COLUMN_NAME_DEGF,
            RasPiContract.RasPiReport.COLUMN_NAME_LIGHT, };

    private static final int COLUMN_TIMESTAMP = 1;
    private static final int COLUMN_DEGF = 2;
    private static final int COLUMN_LIGHT = 3;

    /**
     * List of Cursor columns to read from when preparing an adapter to populate the ListView.
     */
    private static final String[] FROM_COLUMNS = new String[] { RasPiContract.RasPiReport.COLUMN_NAME_TIMESTAMP,
            RasPiContract.RasPiReport.COLUMN_NAME_DEGF, RasPiContract.RasPiReport.COLUMN_NAME_LIGHT, };

    /**
     * List of Views which will be populated by Cursor data.
     */
    private static final int[] TO_FIELDS = new int[] { R.id.timestamp, R.id.degF, R.id.lights, };

    private Dialog mDialog;
    private Registration mRegService;
    private Object mSyncObserverHandle;
    private Menu mOptionsMenu;
    private SimpleCursorAdapter mAdapter;
    private int mLightThreshold;
    private LineGraphView mGraph;

    @InjectView(R.id.temp_list)
    protected ListView mListView;

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.current_temp);
        ButterKnife.inject(this);

        mAdapter = new SimpleCursorAdapter(this, R.layout.record, null, FROM_COLUMNS, TO_FIELDS, 0);
        mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
            @Override
            public boolean setViewValue(final View view, final Cursor cursor, final int columnIndex) {
                final TextView textView = (TextView) view;
                switch (columnIndex) {
                case COLUMN_TIMESTAMP:
                    // Convert timestamp to human-readable date
                    textView.setText(DateUtils.formatDateTime(getApplicationContext(), cursor.getLong(columnIndex),
                            AppConstants.DATE_FORMAT_FLAGS));
                    return true;
                case COLUMN_DEGF:
                    // Restrict to one decimal place
                    textView.setText(String.format("%.1f", cursor.getFloat(columnIndex)));
                    return true;
                case COLUMN_LIGHT:
                    if (cursor.getInt(columnIndex) < mLightThreshold) {
                        textView.setText(getText(R.string.lights_on));
                    } else {
                        textView.setText(getText(R.string.lights_off));
                    }
                    return true;
                }
                return false;
            }
        });

        final View header = getLayoutInflater().inflate(R.layout.record_header, mListView, false);
        final FrameLayout frameLayout = ButterKnife.findById(header, R.id.graph_placeholder);
        mGraph = new LineGraphView(this, "");
        mGraph.setDrawBackground(true);
        mGraph.setBackgroundColor(getResources().getColor(R.color.primary_graph));
        mGraph.setCustomLabelFormatter(new CustomLabelFormatter() {
            @Override
            public String formatLabel(final double value, final boolean isValueX) {
                if (isValueX) {
                    return DateUtils.formatDateTime(getApplicationContext(), (long) value,
                            AppConstants.DATE_FORMAT_FLAGS_GRAPH);
                }
                return String.format(Locale.getDefault(), "%.1f", value);
            }
        });
        mGraph.getGraphViewStyle().setNumHorizontalLabels(AppConstants.GRAPH_NUM_HORIZONTAL_LABELS);
        frameLayout.addView(mGraph);

        mListView.addHeaderView(header, null, false);
        mListView.setAdapter(mAdapter);
        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mSyncStatusObserver.onStatusChanged(0);

        if (checkPlayServices()) {
            final GoogleAccountCredential credential = AccountUtils.getCredential(this);
            if (credential.getSelectedAccountName() != null) {
                GCMHelper.registerIfNeeded(getApplicationContext(), AppConstants.GCM_SENDER_ID, this);
            } else {
                startActivityForResult(credential.newChooseAccountIntent(), ACCOUNT_PICKER_REQUEST);
            }

            // Watch for sync state changes
            final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING | ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
            mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {
        mOptionsMenu = menu;
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_settings:
            startActivity(new Intent(this, Settings.class));
            return true;
        case R.id.action_refresh:
            if (!SyncAdapter.requestSync(this, true)) {
                Toast.makeText(this, R.string.error_refresh, Toast.LENGTH_LONG).show();
            }
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mSyncObserverHandle != null) {
            ContentResolver.removeStatusChangeListener(mSyncObserverHandle);
            mSyncObserverHandle = null;
        }
    }

    @Override
    protected void onDestroy() {
        // Hide dialogs to prevent window leaks on orientation changes
        if ((mDialog != null) && mDialog.isShowing()) {
            mDialog.dismiss();
        }

        super.onDestroy();
    }

    @Override
    protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        switch (requestCode) {
        case ACCOUNT_PICKER_REQUEST:
            if ((data != null) && (data.getExtras() != null)) {
                final String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
                if (!TextUtils.isEmpty(accountName)) {
                    Timber.d("User selected " + accountName);
                    AccountUtils.setAccount(this, accountName);
                }
            }
            break;
        default:
            super.onActivityResult(requestCode, resultCode, data);
            break;
        }
    }

    /**
     * Check the device to make sure it has the Google Play Services APK. If
     * it doesn't, display a dialog that allows users to download the APK from
     * the Google Play Store or enable it in the device's system settings.
     */
    private boolean checkPlayServices() {
        final int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (resultCode != ConnectionResult.SUCCESS) {
            if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
                if ((mDialog != null) && mDialog.isShowing()) {
                    mDialog.dismiss();
                }

                mDialog = GooglePlayServicesUtil.getErrorDialog(resultCode, this, GPS_REQUEST,
                        new DialogInterface.OnCancelListener() {
                            @Override
                            public void onCancel(final DialogInterface dialog) {
                                finish();
                            }
                        });
                mDialog.show();
            } else {
                Timber.e("This device is not supported.");
                finish();
            }
            return false;
        }
        return true;
    }

    @Override
    public boolean onSendRegistrationIdToBackend(final Context context, final String regId) throws IOException {
        Timber.d("Device registered with GCM: regId = " + regId);

        if (mRegService == null) {
            mRegService = new Registration.Builder(AppConstants.HTTP_TRANSPORT, AppConstants.JSON_FACTORY,
                    AccountUtils.getCredential(this)).setApplicationName(context.getString(R.string.app_name))
                            .build();
        }

        mRegService.register(regId).execute();
        return true;
    }

    /**
     * Set the state of the Refresh button. If a sync is active, turn on the ProgressBar widget.
     * Otherwise, turn it off.
     *
     * @param refreshing True if an active sync is occurring, false otherwise
     */
    public void setRefreshActionButtonState(final boolean refreshing) {
        if (mOptionsMenu == null) {
            return;
        }

        final MenuItem refreshItem = mOptionsMenu.findItem(R.id.action_refresh);
        if (refreshItem != null) {
            if (refreshing) {
                refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);
            } else {
                refreshItem.setActionView(null);
            }
        }
    }

    /**
     * Create a new anonymous SyncStatusObserver. It's attached to the app's ContentResolver in
     * onResume(), and removed in onPause(). If status changes, it sets the state of the Refresh
     * button. If a sync is active or pending, the Refresh button is replaced by an indeterminate
     * ProgressBar; otherwise, the button itself is displayed.
     */
    private final SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
        /** Callback invoked when the sync adapter status changes. */
        @Override
        public void onStatusChanged(final int which) {
            runOnUiThread(new Runnable() {
                /**
                 * The SyncAdapter runs on a background thread. To update the UI, onStatusChanged()
                 * runs on the UI thread.
                 */
                @Override
                public void run() {
                    final Account account = AccountUtils.getAccount(getApplicationContext());
                    if (account == null) {
                        // This shouldn't happen, but set the status to "not refreshing".
                        setRefreshActionButtonState(false);
                        return;
                    }

                    // Test the ContentResolver to see if the sync adapter is active or pending.
                    // Set the state of the refresh button accordingly.
                    final boolean syncActive = ContentResolver.isSyncActive(account,
                            RasPiContract.CONTENT_AUTHORITY);
                    final boolean syncPending = ContentResolver.isSyncPending(account,
                            RasPiContract.CONTENT_AUTHORITY);
                    setRefreshActionButtonState(syncActive || syncPending);
                }
            });
        }
    };

    /**
     * Query the content provider for data.
     *
     * <p>Loaders do queries in a background thread. They also provide a ContentObserver that is
     * triggered when data in the content provider changes. When the sync adapter updates the
     * content provider, the ContentObserver responds by resetting the loader and then reloading
     * it.
     */
    @Override
    public Loader<Cursor> onCreateLoader(final int id, final Bundle bundle) {
        return new CursorLoader(this, // Context
                RasPiContract.RasPiReport.CONTENT_URI, // URI
                PROJECTION, // Projection
                null, // Selection
                null, // Selection args
                RasPiContract.RasPiReport.SORT_NEWEST_FIRST // Sort
        );
    }

    /**
     * Move the Cursor returned by the query into the ListView adapter. This refreshes the existing
     * UI with the data in the Cursor.
     */
    @Override
    public void onLoadFinished(final Loader<Cursor> loader, final Cursor cursor) {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        mLightThreshold = prefs.getInt(AppConstants.PREF_THRES_LIGHT, 0);

        mGraph.removeAllSeries();
        final List<GraphView.GraphViewData> data = new ArrayList<>();
        cursor.moveToLast();
        while (cursor.moveToPrevious()) {
            data.add(new GraphView.GraphViewData(cursor.getLong(COLUMN_TIMESTAMP), cursor.getFloat(COLUMN_DEGF)));
        }
        final GraphViewSeries exampleSeries = new GraphViewSeries(
                data.toArray(new GraphView.GraphViewData[data.size()]));
        mGraph.addSeries(exampleSeries);

        mAdapter.changeCursor(cursor);
    }

    /**
     * Called when the ContentObserver defined for the content provider detects that data has
     * changed. The ContentObserver resets the loader, and then re-runs the loader. In the adapter,
     * set the Cursor value to null. This removes the reference to the Cursor, allowing it to be
     * garbage-collected.
     */
    @Override
    public void onLoaderReset(final Loader<Cursor> loader) {
        mAdapter.changeCursor(null);
        mGraph.removeAllSeries();
    }
}