/** * Copyright (C) 2013 - 2015 the enviroCar community * <p> * This file is part of the enviroCar app. * <p> * The enviroCar app 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, either version 3 of the License, or * (at your option) any later version. * <p> * The enviroCar app 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. * <p> * You should have received a copy of the GNU General Public License along * with the enviroCar app. If not, see http://www.gnu.org/licenses/. */ package org.envirocar.app; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.widget.Toolbar; import android.text.Html; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.TextView; import com.afollestad.materialdialogs.MaterialDialog; import com.squareup.otto.Subscribe; import org.envirocar.app.activity.DialogUtil; import org.envirocar.app.handler.BluetoothHandler; import org.envirocar.app.handler.CarPreferenceHandler; import org.envirocar.app.handler.PreferenceConstants; import org.envirocar.app.handler.PreferencesHandler; import org.envirocar.app.handler.TemporaryFileManager; import org.envirocar.app.handler.UserHandler; import org.envirocar.app.services.OBDConnectionService; import org.envirocar.app.services.SystemStartupService; import org.envirocar.app.view.HelpActivity; import org.envirocar.app.view.LoginActivity; import org.envirocar.app.view.SendLogFileFragment; import org.envirocar.app.view.TroubleshootingFragment; import org.envirocar.app.view.dashboard.DashboardMainFragment; import org.envirocar.app.view.logbook.LogbookActivity; import org.envirocar.app.view.settings.SettingsActivity; import org.envirocar.app.view.tracklist.TrackListPagerFragment; import org.envirocar.core.entity.Announcement; import org.envirocar.core.entity.User; import org.envirocar.core.events.NewUserSettingsEvent; import org.envirocar.core.events.TrackFinishedEvent; import org.envirocar.core.events.bluetooth.BluetoothStateChangedEvent; import org.envirocar.core.exception.DataRetrievalFailureException; import org.envirocar.core.exception.NoMeasurementsException; import org.envirocar.core.exception.NotConnectedException; import org.envirocar.core.injection.BaseInjectorActivity; import org.envirocar.core.logging.Logger; import org.envirocar.core.util.Util; import org.envirocar.core.util.VersionRange; import org.envirocar.obd.events.BluetoothServiceStateChangedEvent; import org.envirocar.obd.service.BluetoothServiceState; import org.envirocar.remote.DAOProvider; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import butterknife.ButterKnife; import butterknife.InjectView; import rx.Scheduler; import rx.android.schedulers.AndroidSchedulers; import rx.subscriptions.CompositeSubscription; /** * Main UI application that cares about the auto-upload, auto-connect and global * UI elements * * @author dewall */ public class BaseMainActivity extends BaseInjectorActivity { private static final Logger LOGGER = Logger.getLogger(BaseMainActivity.class); public static final int TRACK_MODE_SINGLE = 0; public static final int TRACK_MODE_AUTO = 1; private static final String TRACK_MODE = "trackMode"; private static final String SEEN_ANNOUNCEMENTS = "seenAnnouncements"; private static final String TROUBLESHOOTING_TAG = "TROUBLESHOOTING"; // Injected variables @Inject protected UserHandler mUserManager; @Inject protected CarPreferenceHandler mCarManager; @Inject protected TemporaryFileManager mTemporaryFileManager; @Inject protected DAOProvider mDAOProvider; @Inject protected BluetoothHandler mBluetoothHandler; @InjectView(R.id.main_layout_toolbar) protected Toolbar mToolbar; @InjectView(R.id.drawer_layout) protected DrawerLayout mDrawerLayout; @InjectView(R.id.nav_drawer_navigation_view) protected NavigationView mNavigationView; @InjectView(R.id.nav_drawer_list_header_layout) protected View mHeaderLayout; @InjectView(R.id.nav_drawer_list_header_username) protected TextView mUsernameText; @InjectView(R.id.nav_drawer_list_header_email) protected TextView mEmailText; private int trackMode = TRACK_MODE_SINGLE; private Set<String> seenAnnouncements = new HashSet<String>(); private BroadcastReceiver errorInformationReceiver; private int selectedMenuItemID; private boolean paused; private ActionBarDrawerToggle mDrawerToggle; private BluetoothServiceState mServiceState = BluetoothServiceState.SERVICE_STOPPED; private Fragment mCurrentFragment; private Fragment mStartupFragment; private CompositeSubscription subscriptions = new CompositeSubscription(); private Scheduler.Worker mMainThreadWorker = AndroidSchedulers.mainThread().createWorker(); @Override protected void onCreate(Bundle savedInstanceState) { /** * try-catch: very dirty hack for broken fragmentmanager impl on some (one?) device */ boolean noInstantiatedExceptionReceived = false; try { super.onCreate(savedInstanceState); } catch (IllegalStateException e) { LOGGER.warn("Trying to reconstruct fragment state. Got Exception", e); if (e.getMessage().contains("No instantiated fragment for index #")) { noInstantiatedExceptionReceived = true; } } // Set the content view of the application setContentView(R.layout.main_layout); mNavigationView = (NavigationView) findViewById(R.id.nav_drawer_navigation_view); LayoutInflater.from(this).inflate(R.layout.nav_drawer_list_header, mNavigationView); ButterKnife.inject(this); // Initializes the Toolbar. setSupportActionBar(mToolbar); getSupportActionBar().setTitle(R.string.menu_nav_drawer_dashboard); if (noInstantiatedExceptionReceived) { TrackListPagerFragment pagerFragment = new TrackListPagerFragment(); if (mNavigationView != null && mNavigationView.getMenu() != null) { MenuItem menuItem = mNavigationView.getMenu().findItem(R.id .menu_nav_drawer_tracklist_new); transitToFragment(menuItem, pagerFragment); } else { LOGGER.warn("Could not re-create TrackListPagerFragment: mNavigationView=" + mNavigationView); } } // Register a listener for a menu item that gets selected. mNavigationView.setNavigationItemSelectedListener(menuItem -> { if (selectDrawerItem(menuItem)) { // we want to have shared checked states between different groups. Therefore, we // cannot use the provided "single" checkedBehavior. For this reason, it is first // required to make the item checkable first. menuItem.setCheckable(true); menuItem.setChecked(true); // Uncheck all other items. Menu m = mNavigationView.getMenu(); for (int i = 0; i < m.size(); i++) { MenuItem mi = m.getItem(i); if (!(mi.getItemId() == menuItem.getItemId())) mi.setChecked(false); } } // Close the navigation drawer. mDrawerLayout.closeDrawers(); return true; }); // Initializes the navigation drawer. initNavigationDrawerLayout(); // Set the DashboardFragment as initial fragment. mStartupFragment = new DashboardMainFragment(); getSupportFragmentManager() .beginTransaction() .replace(R.id.content_frame, mStartupFragment, mStartupFragment.getClass() .getSimpleName()) .commit(); // Subscribe for preference subscriptions. In this case, subscribe for changes to the // active screen settings. // TODO addPreferenceSubscriptions(); errorInformationReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (paused) { return; } Fragment fragment = getSupportFragmentManager().findFragmentByTag (TROUBLESHOOTING_TAG); if (fragment == null) { fragment = new TroubleshootingFragment(); } fragment.setArguments(intent.getExtras()); getSupportFragmentManager().beginTransaction() .replace(R.id.content_frame, fragment) .commit(); } }; registerReceiver(errorInformationReceiver, new IntentFilter(TroubleshootingFragment .INTENT)); resolvePersistentSeenAnnouncements(); } private void addPreferenceSubscriptions() { // Keep screen active setting; subscriptions.add( PreferencesHandler.getDisplayStaysActiveObservable(this) .observeOn(AndroidSchedulers.mainThread()) .subscribe(aBoolean -> { checkKeepScreenOn(); }) ); // Start Background handler subscriptions.add( PreferencesHandler.getBackgroundHandlerEnabledObservable(this) .observeOn(AndroidSchedulers.mainThread()) .subscribe(aBoolean -> { if (aBoolean) { SystemStartupService.startService(this); } else { SystemStartupService.stopService(this); } }) ); } @Subscribe public void onReceiveBluetoothStateChangedEvent(BluetoothStateChangedEvent event) { LOGGER.info(String.format("Received event: %s", event.toString())); updateStartStopButton(); } @Subscribe public void onReceiveBluetoothServiceStateChangedEvent( BluetoothServiceStateChangedEvent event) { LOGGER.info(String.format("Received event: %s", event.toString())); this.mServiceState = event.mState; mMainThreadWorker.schedule(() -> updateStartStopButton()); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); mDrawerToggle.syncState(); } @Override protected void onStart() { LOGGER.info("onStart()"); super.onStart(); // Update the header. updateNavDrawerAccountHeader(); } @Override protected void onResume() { LOGGER.info("onResume()"); super.onResume(); // Check whether the screen is required to keep the screen on. checkKeepScreenOn(); } @Override protected void onPause() { LOGGER.info("onResume()"); super.onPause(); this.paused = false; //first init firstInit(); checkKeepScreenOn(); // new AsyncTask<Void, Void, Void>() { // @Override // protected Void doInBackground(Void... params) { // checkAffectingAnnouncements(); // return null; // } // }.execute(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); } @Override protected void onDestroy() { super.onDestroy(); this.unregisterReceiver(errorInformationReceiver); mTemporaryFileManager.shutdown(); if (!subscriptions.isUnsubscribed()) { subscriptions.unsubscribe(); } } @Override public boolean onOptionsItemSelected(MenuItem item) { // Open or close the drawer. switch (item.getItemId()) { case android.R.id.home: mDrawerLayout.openDrawer(GravityCompat.START); return true; } if (mDrawerToggle.onOptionsItemSelected(item)) return true; return super.onOptionsItemSelected(item); } private void checkAffectingAnnouncements() { final List<Announcement> annos = new ArrayList<>(); try { annos.addAll(mDAOProvider.getAnnouncementsDAO().getAllAnnouncements()); } catch (DataRetrievalFailureException e) { LOGGER.warn(e.getMessage(), e); return; } catch (NotConnectedException e) { e.printStackTrace(); } final VersionRange.Version version; try { String versionShort = Util.getVersionStringShort(getApplicationContext()); version = VersionRange.Version.fromString(versionShort); } catch (PackageManager.NameNotFoundException e) { LOGGER.warn(e.getMessage()); return; } runOnUiThread(() -> { for (Announcement announcement : annos) { if (!seenAnnouncements.contains(announcement.getId())) { if (announcement.getVersionRange().isInRange(version)) { showAnnouncement(announcement); } } } }); } private void showAnnouncement(final Announcement announcement) { String priorityi18n; if (announcement.getPriority().equals(Announcement.Priority.HIGH)) { priorityi18n = this.getString(R.string.category_high); } else if (announcement.getPriority().equals(Announcement.Priority.MEDIUM)) { priorityi18n = this.getString(R.string.category_normal); } else { priorityi18n = this.getString(R.string.category_low); } String title = String.format("[%s] %s %s", priorityi18n, announcement.getPriority(), this .getString(R.string .announcement)); String content = announcement.getContent().getAsString(); DialogUtil.createTitleMessageInfoDialog(title, Html.fromHtml(content), true, new DialogUtil.PositiveNegativeCallback() { @Override public void negative() { seenAnnouncements.add(announcement.getId()); } @Override public void positive() { addPersistentSeenAnnouncement(announcement.getId()); seenAnnouncements.add(announcement.getId()); } }, this); } protected void addPersistentSeenAnnouncement(String id) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); String currentPersisted = preferences.getString(PreferenceConstants .PERSISTENT_SEEN_ANNOUNCEMENTS, ""); StringBuilder sb = new StringBuilder(currentPersisted); if (!currentPersisted.isEmpty()) { sb.append(","); } sb.append(id); preferences.edit().putString( PreferenceConstants.PERSISTENT_SEEN_ANNOUNCEMENTS, sb.toString()).commit(); } // TODO check private void firstInit() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); if (!preferences.contains("first_init")) { // mDrawerLayout.openDrawer(mDrawerList); SharedPreferences.Editor e = preferences.edit(); e.putString("first_init", "seen"); e.putBoolean("pref_privacy", true); e.commit(); } } private void initNavigationDrawerLayout() { // Initialize the navigation drawer mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); updateStartStopButton(); // Initializes the toggle for the navigation drawer. mDrawerToggle = new ActionBarDrawerToggle( this, mDrawerLayout, mToolbar, R.string.open_drawer, R.string.close_drawer) { @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); } }; mDrawerLayout.setDrawerListener(mDrawerToggle); // Enables the home button. getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); mHeaderLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(BaseMainActivity.this, LoginActivity.class); startActivity(intent); } }); } /** * Called when an item of the navigation drawer has been clicked. This method is responsible * to replace the visible fragment with the corresponding fragment of the clicked item. * * @param menuItem the item clicked in the menu of the navigation drawer. */ private boolean selectDrawerItem(MenuItem menuItem) { LOGGER.info(String.format("selectDrawerItem(%s)", menuItem.getTitle())); Fragment fragment = null; switch (menuItem.getItemId()) { case R.id.menu_nav_drawer_dashboard: // fragment = new RealDashboardFragment(); fragment = new DashboardMainFragment(); break; case R.id.menu_nav_drawer_tracklist_new: fragment = new TrackListPagerFragment(); break; case R.id.menu_nav_drawer_logbook: Intent intent3 = new Intent(BaseMainActivity.this, LogbookActivity.class); startActivity(intent3); return false; case R.id.menu_nav_drawer_account_login: Intent intent = new Intent(BaseMainActivity.this, LoginActivity.class); startActivity(intent); return false; case R.id.menu_nav_drawer_settings_general: Intent intent2 = new Intent(BaseMainActivity.this, SettingsActivity.class); startActivity(intent2); return false; case R.id.menu_nav_drawer_settings_help: Intent help = new Intent(BaseMainActivity.this, HelpActivity.class); startActivity(help); return false; case R.id.menu_nav_drawer_settings_sendlog: fragment = new SendLogFileFragment(); break; case R.id.menu_nav_drawer_quit_app: new MaterialDialog.Builder(this) .title(getString(R.string.menu_close_envirocar_title)) .positiveText(getString(R.string.menu_close_envirocar_positive)) .negativeText(getString(R.string.cancel)) .content(getString(R.string.menu_close_envirocar_content)) .callback(new MaterialDialog.ButtonCallback() { @Override public void onPositive(MaterialDialog dialog) { shutdownEnviroCar(); } }) .show(); return false; } // If the fragment is null or the fragment is already visible, then do nothing. if (fragment == null || isFragmentVisible(fragment.getClass().getSimpleName())) return false; //now do the transition transitToFragment(menuItem, fragment); return true; } private void transitToFragment(MenuItem menuItem, Fragment fragment) { // Insert the fragment by replacing the existent fragment in the content frame. replaceFragment(fragment, selectedMenuItemID > menuItem.getItemId() ? R.anim.translate_slide_in_left_fragment : R.anim.translate_slide_in_right_fragment, selectedMenuItemID > menuItem.getItemId() ? R.anim.translate_slide_out_right_fragment : R.anim.translate_slide_out_left_fragment); mCurrentFragment = fragment; selectedMenuItemID = menuItem.getItemId(); /// update the title of the toolbar. setTitle(menuItem.getTitle()); } private void shutdownEnviroCar() { SystemStartupService.stopService(this); OBDConnectionService.stopService(this); mMainThreadWorker.schedule(() -> { System.runFinalizersOnExit(true); System.exit(0); }, 750, TimeUnit.MILLISECONDS); } /** * @param fragment * @param animIn * @param animOut */ private void replaceFragment(Fragment fragment, int animIn, int animOut) { FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); if (animIn != -1 && animOut != -1) { ft.setCustomAnimations(animIn, animOut); } ft.replace(R.id.content_frame, fragment, fragment.getClass().getSimpleName()); // ft.addToBackStack(null); ft.commit(); } @Override public List<Object> getInjectionModules() { return Arrays.<Object>asList(new MainActivityModule(this)); } @Subscribe public void onReceiveTrackFinishedEvent(final TrackFinishedEvent event) { LOGGER.info(String.format("onReceiveTrackFinishedEvent(): event=%s", event.toString())); // Just show a message depending on the event-related track. mMainThreadWorker.schedule(() -> { if (event.mTrack == null) { // Track is null and thus there was an error. showSnackbar(R.string.track_finishing_failed); } else try { if (event.mTrack.getLastMeasurement() != null) { LOGGER.info("last is not null.. " + event.mTrack.getLastMeasurement() .toString()); // Track has no measurements showSnackbar(getString(R.string.track_finished).concat(event.mTrack.getName())); } } catch (NoMeasurementsException e) { LOGGER.warn("Track has been finished without measurements", e); // Track has no measurements showSnackbar(R.string.track_finished_no_measurements); } }); } @Subscribe public void onReceiveNewUserSettingsEvent(NewUserSettingsEvent event) { LOGGER.info(String.format("onReceiveNewUserSettingsEvent(): event=%s", event.toString())); updateNavDrawerAccountHeader(); } private void updateNavDrawerAccountHeader() { runOnUiThread(() -> { boolean isLoggedIn = mUserManager.isLoggedIn(); User user = mUserManager.getUser(); if (isLoggedIn) { mUsernameText.setText(user.getUsername()); mEmailText.setText(user.getMail()); } else { mUsernameText.setText(R.string.menu_not_logged_in); mEmailText.setText(R.string.menu_not_logged_in_sub); } }); } /** * This method checks, whether a Fragment with a certain tag is visible. * * @param tag The tag of the Fragment. * @return True if the Fragment is visible, false if not. */ public boolean isFragmentVisible(String tag) { Fragment tmpFragment = getSupportFragmentManager().findFragmentByTag(tag); if (tmpFragment != null && tmpFragment.isVisible()) { LOGGER.info("Fragment with tag: " + tag + " is already visible."); return true; } return false; } private void checkKeepScreenOn() { if (PreferenceManager .getDefaultSharedPreferences(this) .getBoolean(PreferenceConstants.DISPLAY_STAYS_ACTIV, false)) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } private void updateStartStopButton() { // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); // StartStopButtonUtil startStopUtil = new StartStopButtonUtil(this, trackMode, // mServiceState, // mServiceState == BluetoothServiceState.SERVICE_DEVICE_DISCOVERY_PENDING); // if (adapter != null && adapter.isEnabled()) { // was requirementsFulfilled // startStopUtil.updateStartStopButtonOnServiceStateChange // (navDrawerItems[START_STOP_MEASUREMENT]); // } else { // startStopUtil.defineButtonContents(navDrawerItems[START_STOP_MEASUREMENT], // false, R.drawable.not_available, getString(R.string // .pref_bluetooth_disabled), // getString(R.string.menu_start)); // } // // mNavDrawerAdapter.notifyDataSetChanged(); } protected void resolvePersistentSeenAnnouncements() { String pers = PreferenceManager.getDefaultSharedPreferences(this) .getString(PreferenceConstants.PERSISTENT_SEEN_ANNOUNCEMENTS, ""); if (!pers.isEmpty()) { if (pers.contains(",")) { String[] arr = pers.split(","); for (String string : arr) { seenAnnouncements.add(string); } } else { seenAnnouncements.add(pers); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(TRACK_MODE, trackMode); outState.putSerializable(SEEN_ANNOUNCEMENTS, this.seenAnnouncements.toArray()); } private void showSnackbar(int infoRes) { showSnackbar(getString(infoRes)); } private void showSnackbar(String info) { mMainThreadWorker.schedule(() -> { if (mDrawerLayout != null) { Snackbar.make(mDrawerLayout, info, Snackbar.LENGTH_LONG).show(); } }); } private void readSavedState(Bundle savedInstanceState) { if (savedInstanceState == null) return; this.trackMode = savedInstanceState.getInt(TRACK_MODE); String[] arr = (String[]) savedInstanceState.getSerializable(SEEN_ANNOUNCEMENTS); if (arr != null) { for (String string : arr) { this.seenAnnouncements.add(string); } } } }