/*
* Copyright 2014 Bevbot LLC <info@bevbot.com>
*
* This file is part of the Kegtab package from the Kegbot project. For
* more information on Kegtab or Kegbot, see <http://kegbot.org/>.
*
* Kegtab is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, version 2.
*
* Kegtab 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with Kegtab. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kegbot.app;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.MenuItem;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.hoho.android.usbserial.util.HexDump;
import com.squareup.otto.Subscribe;
import org.kegbot.app.alert.AlertCore;
import org.kegbot.app.config.AppConfiguration;
import org.kegbot.app.event.ConnectivityChangedEvent;
import org.kegbot.app.event.VisibleTapsChangedEvent;
import org.kegbot.app.service.CheckinService;
import org.kegbot.app.util.SortableFragmentStatePagerAdapter;
import org.kegbot.app.util.Utils;
import org.kegbot.core.KegbotCore;
import org.kegbot.proto.Models.KegTap;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* The main "home screen" of the Kegtab application. It shows the status of each tap, and allows the
* user start a pour by authenticating (if enabled in settings).
*/
public class HomeActivity extends CoreActivity {
private static final String LOG_TAG = HomeActivity.class.getSimpleName();
//private static final int REQUEST_PLAY_SERVICES_UPDATE = 100;
private static final String GCM_SENDER_ID = "209039242857";
private static final String ACTION_SHOW_TAP_EDITOR = "show_editor";
private static final String EXTRA_METER_NAME = "meter_name";
private static final String ALERT_ID_UNBOUND_TAPS = "unbound-taps";
/**
* Idle timeout which triggers "attract mode".
*
* @see #mAttractModeRunnable
*/
private static final long IDLE_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10);
/**
* Pause interval between rotated screens in "attract mode".
*
* @see #mAttractModeRunnable
*/
private static final long ROTATE_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(12);
private static final Function<KegTap, String> TAP_TO_NAME = new Function<KegTap, String>() {
@Nullable
@Override
public String apply(@Nullable KegTap input) {
return input != null ? input.getName() : "none";
}
};
private KegbotCore mCore;
private HomeFragmentsAdapter mTapStatusAdapter;
private ViewPager mTapStatusPager;
private AppConfiguration mConfig;
/**
* Keep track of Google Play Services error codes, and don't annoy when the same error persists.
* (For some reason, {@link GooglePlayServicesUtil} treats absence of the apk as "user
* recoverable").
*
* @see #checkPlayServices()
*/
private int mLastShownGooglePlayServicesError = Integer.MIN_VALUE;
private final Object mTapsLock = new Object();
/**
* Shadow copy of tap manager taps.
*/
@GuardedBy("mTapsLock")
private final List<KegTap> mTaps = Lists.newArrayList();
/** Main thread handler for managing {@link #mAttractModeRunnable}. */
private final Handler mAttractModeHandler = new Handler(Looper.getMainLooper());
/**
* Rotates through view pager when idle.
*
* @see #startAttractMode()
* @see #resetAttractMode()
* @see #cancelAttractMode()
*/
private final Runnable mAttractModeRunnable = new Runnable() {
@Override
public void run() {
rotateDisplay();
mAttractModeHandler.postDelayed(mAttractModeRunnable, ROTATE_INTERVAL_MILLIS);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
synchronized (mTapsLock) {
mTaps.clear();
}
mTapStatusAdapter = new HomeFragmentsAdapter(getFragmentManager());
mTapStatusPager = (ViewPager) findViewById(R.id.tap_status_pager);
mTapStatusPager.setAdapter(mTapStatusAdapter);
mTapStatusPager.setOffscreenPageLimit(8); // >8 Tap systems are rare
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onStart() {
super.onStart();
mCore = KegbotCore.getInstance(this);
mConfig = mCore.getConfiguration();
maybeShowTapWarnings();
}
@Override
protected void onResume() {
Log.d(LOG_TAG, "onResume");
super.onResume();
mCore.getBus().register(this);
mCore.getHardwareManager().refreshSoon();
startAttractMode();
if (checkPlayServices()) {
doGcmRegistration();
}
}
@Override
protected void onPause() {
Log.d(LOG_TAG, "onPause");
mCore.getBus().unregister(this);
cancelAttractMode();
super.onPause();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
switch (itemId) {
case R.id.settings:
SettingsActivity.startSettingsActivity(this);
return true;
case R.id.manageTaps:
TapListActivity.startActivity(this);
return true;
case R.id.bugreport:
BugreportActivity.startBugreportActivity(this);
return true;
case android.R.id.home:
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onNewIntent(Intent intent) {
Log.d(LOG_TAG, "onNewIntent: Got intent: " + intent);
if (intent.hasExtra(NfcAdapter.EXTRA_TAG)) {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
byte[] id = tag.getId();
if (id != null && id.length > 0) {
String tagId = HexDump.toHexString(id).toLowerCase(Locale.US);
Log.d(LOG_TAG, "Read NFC tag with id: " + tagId);
// TODO: use tag technology as part of id?
AuthenticatingActivity.startAndAuthenticate(this, "nfc", tagId);
}
}
}
@Subscribe
public void onVisibleTapListUpdate(VisibleTapsChangedEvent event) {
assert(Looper.myLooper() == Looper.getMainLooper());
Log.d(LOG_TAG, "Got tap list change event: " + event + " taps=" + event.getTaps().size());
final List<KegTap> newTapList = event.getTaps();
synchronized (mTapsLock) {
if (newTapList.equals(mTaps)) {
Log.d(LOG_TAG, "Tap list unchanged.");
return;
}
mTaps.clear();
mTaps.addAll(newTapList);
mTapStatusAdapter.notifyDataSetChanged();
}
maybeShowTapWarnings();
}
private void maybeShowTapWarnings() {
final List<KegTap> unboundTaps = Lists.newArrayList();
synchronized (mTapsLock) {
for (final KegTap tap : mTaps) {
if (!tap.hasMeter()) {
unboundTaps.add(tap);
}
}
}
if (unboundTaps.isEmpty()) {
mCore.getAlertCore().cancelAlert(ALERT_ID_UNBOUND_TAPS);
return;
}
final String message;
final List<String> tapNames = Lists.transform(unboundTaps, TAP_TO_NAME);
if (tapNames.size() == 1) {
message = getString(R.string.alert_unbound_single_tap_description,
tapNames.get(0));
} else {
final String listStr = Joiner.on(", ").join(tapNames.subList(0, tapNames.size() - 1));
message = getString(R.string.alert_unbound_multiple_taps_description, listStr,
tapNames.get(tapNames.size() - 1));
}
mCore.getAlertCore().postAlert(AlertCore.newBuilder(getString(R.string.alert_unbound_title))
.setId(ALERT_ID_UNBOUND_TAPS)
.setAction(new Runnable() {
@Override
public void run() {
TapListActivity.startActivity(getApplicationContext());
}
})
.setActionName(getString(R.string.alert_unbound_action_name))
.setDescription(message)
.severityWarning()
.build());
}
@Subscribe
public void onConnectivityChangedEvent(ConnectivityChangedEvent event) {
updateConnectivityAlert(event.isConnected());
}
/**
* Shows the tap editor for the given tap, prompting for the manager pin if necessary.
*
* @param context
* @param meterName
*/
static void showTapEditor(Context context, String meterName) {
final Intent editorIntent = new Intent(context, HomeActivity.class);
editorIntent.setAction(ACTION_SHOW_TAP_EDITOR);
editorIntent.putExtra(EXTRA_METER_NAME, meterName);
editorIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
PinActivity.startThroughPinActivity(context, editorIntent);
}
@Override
public void onUserInteraction() {
resetAttractMode();
}
private void rotateDisplay() {
final int count = mTapStatusAdapter.getCount();
if (count <= 1) {
return;
}
final int nextItem = (mTapStatusPager.getCurrentItem() + 1) % mTapStatusAdapter.getCount();
mTapStatusPager.setCurrentItem(nextItem);
}
private void startAttractMode() {
cancelAttractMode();
if (mConfig.getEnableAttractMode()) {
mAttractModeHandler.postDelayed(mAttractModeRunnable, IDLE_TIMEOUT_MILLIS);
}
}
private void resetAttractMode() {
cancelAttractMode();
startAttractMode();
}
private void cancelAttractMode() {
mAttractModeHandler.removeCallbacks(mAttractModeRunnable);
}
private void doGcmRegistration() {
final int versionCode = Utils.getOwnPackageInfo(getApplicationContext()).versionCode;
final int registeredVersionCode = mConfig.getGcmRegistrationAppVersion();
final String currentRegId = mConfig.getGcmRegistrationId();
// Fast path: reuse saved id.
if (versionCode == registeredVersionCode && !Strings.isNullOrEmpty(currentRegId)) {
return;
}
// Destroy stale regid, if any.
mConfig.setGcmRegistrationId("");
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Log.d(LOG_TAG, "Registering for GCM ...");
final GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(HomeActivity.this);
final String gcmId;
try {
gcmId = gcm.register(GCM_SENDER_ID);
} catch (IOException e) {
Log.w(LOG_TAG, "GCM registration failed.", e);
return null;
}
mConfig.setGcmRegistrationId(gcmId);
mConfig.setGcmRegistrationAppVersion(versionCode);
CheckinService.requestImmediateCheckin(getApplicationContext());
Log.d(LOG_TAG, "GCM registration success, id=" + gcmId);
return null;
}
}.execute(null, null, null);
}
private boolean checkPlayServices() {
int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
Log.i(LOG_TAG, "GCM error: " + resultCode);
if (resultCode != mLastShownGooglePlayServicesError) {
Log.w(LOG_TAG, GooglePlayServicesUtil.getErrorString(resultCode));
//GooglePlayServicesUtil.getErrorDialog(
// resultCode, this, REQUEST_PLAY_SERVICES_UPDATE).show();
mLastShownGooglePlayServicesError = resultCode;
}
}
return false;
}
return true;
}
/**
* Shows a TapStatusFragment for each tap, plus a SystemStatusFragment.
*/
public class HomeFragmentsAdapter extends SortableFragmentStatePagerAdapter {
public HomeFragmentsAdapter(FragmentManager fm) {
super(fm);
}
@Override
public long getItemId(int position) {
if (position < mTaps.size()) {
return mTaps.get(position).getId();
} else if (position == mTaps.size()) {
return -1;
}
throw new IndexOutOfBoundsException("Position out of bounds: " + position);
}
@Override
public Fragment getItem(int index) {
Log.d(LOG_TAG, "getItem: " + index);
synchronized (mTapsLock) {
if (index < mTaps.size()) {
final KegTap tap = mTaps.get(index);
TapStatusFragment frag = TapStatusFragment.forTap(mTaps.get(index));
return frag;
} else if (index == mTaps.size()) {
SystemStatusFragment frag = new SystemStatusFragment();
return frag;
} else {
Log.wtf(LOG_TAG, "Trying to get fragment " + index + ", current size " + mTaps.size());
return null;
}
}
}
@Override
public int getItemPosition(Object object) {
Log.d(LOG_TAG, "getItemPosition: " + object);
synchronized (mTapsLock) {
if (object instanceof SystemStatusFragment) {
Log.d(LOG_TAG, " position=" + mTaps.size());
return mTaps.size();
}
if (object instanceof TapStatusFragment) {
final int tapId = ((TapStatusFragment) object).getTapId();
int position = 0;
for (final KegTap tap : mTaps) {
if (tap.getId() == tapId) {
Log.d(LOG_TAG, " position=" + position);
return position;
}
position++;
}
}
}
Log.d(LOG_TAG, " position=NONE");
return POSITION_NONE;
}
@Override
public int getCount() {
synchronized (mTapsLock) {
return mTaps.size() + 1;
}
}
@Override
public float getPageWidth(int position) {
return 0.5f;
}
}
}