package cgeo.geocaching; import cgeo.geocaching.activity.AbstractActionBarActivity; import cgeo.geocaching.address.AndroidGeocoder; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.capability.ILogin; import cgeo.geocaching.connector.gc.PocketQueryListActivity; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.helper.UsefulAppsActivity; import cgeo.geocaching.list.PseudoList; import cgeo.geocaching.list.StoredList; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.location.Units; import cgeo.geocaching.maps.DefaultMap; import cgeo.geocaching.network.Network; import cgeo.geocaching.playservices.AppInvite; import cgeo.geocaching.sensors.GeoData; import cgeo.geocaching.sensors.GeoDirHandler; import cgeo.geocaching.sensors.GpsStatusProvider; import cgeo.geocaching.sensors.GpsStatusProvider.Status; import cgeo.geocaching.sensors.Sensors; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.storage.LocalStorage; import cgeo.geocaching.ui.WeakReferenceHandler; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.DatabaseBackupUtils; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.TextUtils; import cgeo.geocaching.utils.Version; import cgeo.geocaching.utils.functions.Action1; import android.app.AlertDialog; import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.location.Address; import android.net.ConnectivityManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView.OnQueryTextListener; import android.support.v7.widget.SearchView.OnSuggestionListener; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import com.jakewharton.processphoenix.ProcessPhoenix; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import org.apache.commons.lang3.StringUtils; public class MainActivity extends AbstractActionBarActivity { @BindView(R.id.nav_satellites) protected TextView navSatellites; @BindView(R.id.filter_button_title) protected TextView filterTitle; @BindView(R.id.map) protected ImageView findOnMap; @BindView(R.id.search_offline) protected ImageView findByOffline; @BindView(R.id.advanced_button) protected ImageView advanced; @BindView(R.id.any_button) protected ImageView any; @BindView(R.id.filter_button) protected ImageView filter; @BindView(R.id.nearest) protected ImageView nearestView; @BindView(R.id.nav_type) protected TextView navType; @BindView(R.id.nav_accuracy) protected TextView navAccuracy; @BindView(R.id.nav_location) protected TextView navLocation; @BindView(R.id.offline_count) protected TextView countBubble; @BindView(R.id.info_area) protected ListView infoArea; /** * view of the action bar search */ private SearchView searchView; private MenuItem searchItem; private Geopoint addCoords = null; private boolean initialized = false; private ConnectivityChangeReceiver connectivityChangeReceiver; private final UpdateLocation locationUpdater = new UpdateLocation(); private final Handler updateUserInfoHandler = new UpdateUserInfoHandler(this); private final Handler firstLoginHandler = new FirstLoginHandler(this); private static final class UpdateUserInfoHandler extends WeakReferenceHandler<MainActivity> { UpdateUserInfoHandler(final MainActivity activity) { super(activity); } @Override public void handleMessage(final Message msg) { final MainActivity activity = getReference(); if (activity != null) { // Get active connectors with login status final ILogin[] loginConns = ConnectorFactory.getActiveLiveConnectors(); // Update UI activity.infoArea.setAdapter(new ArrayAdapter<ILogin>(activity, R.layout.main_activity_connectorstatus, loginConns) { @Override public View getView(final int position, final View convertView, final android.view.ViewGroup parent) { TextView rowView = (TextView) convertView; if (rowView == null) { rowView = (TextView) activity.getLayoutInflater().inflate(R.layout.main_activity_connectorstatus, parent, false); } final ILogin connector = getItem(position); fillView(rowView, connector); return rowView; } private void fillView(final TextView connectorInfo, final ILogin conn) { final StringBuilder userInfo = new StringBuilder(conn.getNameAbbreviated()).append(Formatter.SEPARATOR); if (conn.isLoggedIn()) { userInfo.append(conn.getUserName()); if (conn.getCachesFound() >= 0) { userInfo.append(" (").append(conn.getCachesFound()).append(')'); } userInfo.append(Formatter.SEPARATOR); } userInfo.append(conn.getLoginStatusString()); connectorInfo.setText(userInfo); connectorInfo.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { SettingsActivity.openForScreen(R.string.preference_screen_services, activity); } }); } }); } } } private final class ConnectivityChangeReceiver extends BroadcastReceiver { private boolean isConnected = Network.isConnected(); @Override public void onReceive(final Context context, final Intent intent) { final boolean wasConnected = isConnected; isConnected = Network.isConnected(); if (isConnected && !wasConnected) { startBackgroundLogin(); } } } private static String formatAddress(final Address address) { final List<String> addressParts = new ArrayList<>(); final String countryName = address.getCountryName(); if (countryName != null) { addressParts.add(countryName); } final String locality = address.getLocality(); if (locality != null) { addressParts.add(locality); } else { final String adminArea = address.getAdminArea(); if (adminArea != null) { addressParts.add(adminArea); } } return StringUtils.join(addressParts, ", "); } private final Consumer<GpsStatusProvider.Status> satellitesHandler = new Consumer<Status>() { @Override public void accept(final Status gpsStatus) { if (gpsStatus.gpsEnabled) { navSatellites.setText(res.getString(R.string.loc_sat) + ": " + gpsStatus.satellitesFixed + '/' + gpsStatus.satellitesVisible); } else { navSatellites.setText(res.getString(R.string.loc_gps_disabled)); } } }; private static final class FirstLoginHandler extends WeakReferenceHandler<MainActivity> { FirstLoginHandler(final MainActivity activity) { super(activity); } @Override public void handleMessage(final Message msg) { final MainActivity activity = getReference(); if (activity != null) { try { final StatusCode reason = (StatusCode) msg.obj; if (reason != null && reason != StatusCode.NO_ERROR) { //LoginFailed activity.showToast(activity.res.getString(reason == StatusCode.MAINTENANCE ? reason.getErrorString() : R.string.err_login_failed_toast)); } } catch (final Exception e) { Log.w("MainActivity.firstLoginHander", e); } } } } @Override public void onCreate(final Bundle savedInstanceState) { // don't call the super implementation with the layout argument, as that would set the wrong theme super.onCreate(savedInstanceState); // Disable the up navigation for this activity getSupportActionBar().setDisplayHomeAsUpEnabled(false); setContentView(R.layout.main_activity); ButterKnife.bind(this); if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { // If we had been open already, start from the last used activity. finish(); return; } setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); // type to search Log.i("Starting " + getPackageName() + ' ' + Version.getVersionCode(this) + " a.k.a " + Version.getVersionName(this)); init(); checkShowChangelog(); if (LocalStorage.isRunningLowOnDiskSpace()) { Dialogs.message(this, res.getString(R.string.init_low_disk_space), res.getString(R.string.init_low_disk_space_message)); } } @Override public void onConfigurationChanged(final Configuration newConfig) { super.onConfigurationChanged(newConfig); init(); } @Override public void onResume() { super.onResume(locationUpdater.start(GeoDirHandler.UPDATE_GEODATA | GeoDirHandler.LOW_POWER), Sensors.getInstance().gpsStatusObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(satellitesHandler)); updateUserInfoHandler.sendEmptyMessage(-1); startBackgroundLogin(); init(); connectivityChangeReceiver = new ConnectivityChangeReceiver(); registerReceiver(connectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } private void startBackgroundLogin() { final boolean mustLogin = ConnectorFactory.mustRelog(); for (final ILogin conn : ConnectorFactory.getActiveLiveConnectors()) { if (mustLogin || !conn.isLoggedIn()) { AndroidRxUtils.networkScheduler.scheduleDirect(new Runnable() { @Override public void run() { if (mustLogin) { // Properly log out from geocaching.com conn.logout(); } conn.login(firstLoginHandler, MainActivity.this); updateUserInfoHandler.sendEmptyMessage(-1); } }); } } } @Override public void onDestroy() { initialized = false; ConnectorFactory.showLoginToast = true; super.onDestroy(); } @Override public void onStop() { initialized = false; super.onStop(); } @Override public void onPause() { initialized = false; unregisterReceiver(connectivityChangeReceiver); super.onPause(); } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.main_activity_options, menu); final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); searchItem = menu.findItem(R.id.menu_gosearch); searchView = (SearchView) MenuItemCompat.getActionView(searchItem); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); hideKeyboardOnSearchClick(searchItem); return true; } private void hideKeyboardOnSearchClick(final MenuItem searchItem) { searchView.setOnSuggestionListener(new OnSuggestionListener() { @Override public boolean onSuggestionSelect(final int arg0) { return false; } @Override public boolean onSuggestionClick(final int arg0) { MenuItemCompat.collapseActionView(searchItem); searchView.setIconified(true); // return false to invoke standard behavior of launching the intent for the search result return false; } }); // Used to collapse searchBar on submit from virtual keyboard searchView.setOnQueryTextListener(new OnQueryTextListener() { @Override public boolean onQueryTextSubmit(final String s) { MenuItemCompat.collapseActionView(searchItem); searchView.setIconified(true); return false; } @Override public boolean onQueryTextChange(final String s) { return false; } }); } @Override public boolean onPrepareOptionsMenu(final Menu menu) { super.onPrepareOptionsMenu(menu); menu.findItem(R.id.menu_pocket_queries).setVisible(Settings.isGCConnectorActive() && Settings.isGCPremiumMember()); menu.findItem(R.id.menu_app_invite).setVisible(AppInvite.isAvailable()); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { final int id = item.getItemId(); switch (id) { case android.R.id.home: // this activity must handle the home navigation different than all others showAbout(null); return true; case R.id.menu_about: showAbout(null); return true; case R.id.menu_helpers: startActivity(new Intent(this, UsefulAppsActivity.class)); return true; case R.id.menu_settings: startActivityForResult(new Intent(this, SettingsActivity.class), Intents.SETTINGS_ACTIVITY_REQUEST_CODE); return true; case R.id.menu_history: startActivity(CacheListActivity.getHistoryIntent(this)); return true; case R.id.menu_scan: startScannerApplication(); return true; case R.id.menu_pocket_queries: if (!Settings.isGCPremiumMember()) { return true; } startActivity(new Intent(this, PocketQueryListActivity.class)); return true; case R.id.menu_app_invite: AppInvite.send(this, getString(R.string.invitation_message)); return true; } return super.onOptionsItemSelected(item); } private void startScannerApplication() { final IntentIntegrator integrator = new IntentIntegrator(this); // integrator dialog is English only, therefore localize it integrator.setButtonYesByID(android.R.string.yes); integrator.setButtonNoByID(android.R.string.no); integrator.setTitleByID(R.string.menu_scan_geo); integrator.setMessageByID(R.string.menu_scan_description); integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES); } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { if (requestCode == Intents.SETTINGS_ACTIVITY_REQUEST_CODE) { if (resultCode == SettingsActivity.RESTART_NEEDED) { ProcessPhoenix.triggerRebirth(this); } } else { final IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (scanResult != null) { final String scan = scanResult.getContents(); if (StringUtils.isBlank(scan)) { return; } SearchActivity.startActivityScan(scan, this); } else if (requestCode == Intents.SEARCH_REQUEST_CODE) { // SearchActivity activity returned without making a search if (resultCode == RESULT_CANCELED) { String query = intent.getStringExtra(SearchManager.QUERY); if (query == null) { query = ""; } Dialogs.message(this, res.getString(R.string.unknown_scan) + "\n\n" + query); } } } } private void setFilterTitle() { filterTitle.setText(Settings.getCacheType().getL10n()); } private void init() { if (initialized) { return; } initialized = true; findOnMap.setClickable(true); findOnMap.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { cgeoFindOnMap(v); } }); findByOffline.setClickable(true); findByOffline.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { cgeoFindByOffline(v); } }); findByOffline.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { new StoredList.UserInterface(MainActivity.this).promptForListSelection(R.string.list_title, new Action1<Integer>() { @Override public void call(final Integer selectedListId) { Settings.setLastDisplayedList(selectedListId); CacheListActivity.startActivityOffline(MainActivity.this); } }, false, PseudoList.HISTORY_LIST.id); return true; } }); findByOffline.setLongClickable(true); advanced.setClickable(true); advanced.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { cgeoSearch(v); } }); any.setClickable(true); any.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { cgeoPoint(v); } }); filter.setClickable(true); filter.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { selectGlobalTypeFilter(); } }); filter.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View v) { Settings.setCacheType(CacheType.ALL); setFilterTitle(); return true; } }); updateCacheCounter(); setFilterTitle(); checkRestore(); DataStore.cleanIfNeeded(this); } protected void selectGlobalTypeFilter() { Dialogs.selectGlobalTypeFilter(this, new Action1<CacheType>() { @Override public void call(final CacheType cacheType) { setFilterTitle(); } }); } public void updateCacheCounter() { AndroidRxUtils.bindActivity(this, DataStore.getAllCachesCountObservable()).subscribe(new Consumer<Integer>() { @Override public void accept(final Integer countBubbleCnt1) { if (countBubbleCnt1 == 0) { countBubble.setVisibility(View.GONE); } else { countBubble.setText(Integer.toString(countBubbleCnt1)); countBubble.bringToFront(); countBubble.setVisibility(View.VISIBLE); } } }, new Consumer<Throwable>() { @Override public void accept(final Throwable throwable) { Log.e("Unable to add bubble count", throwable); } }); } private void checkRestore() { if (!DataStore.isNewlyCreatedDatebase() || DatabaseBackupUtils.getRestoreFile() == null) { return; } new AlertDialog.Builder(this) .setTitle(res.getString(R.string.init_backup_restore)) .setMessage(res.getString(R.string.init_restore_confirm)) .setCancelable(false) .setPositiveButton(getString(android.R.string.yes), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int id) { dialog.dismiss(); DataStore.resetNewlyCreatedDatabase(); DatabaseBackupUtils.restoreDatabase(MainActivity.this); } }) .setNegativeButton(getString(android.R.string.no), new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int id) { dialog.cancel(); DataStore.resetNewlyCreatedDatabase(); } }) .create() .show(); } private class UpdateLocation extends GeoDirHandler { @Override public void updateGeoData(final GeoData geo) { if (!nearestView.isClickable()) { nearestView.setFocusable(true); nearestView.setClickable(true); nearestView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { cgeoFindNearest(v); } }); nearestView.setBackgroundResource(R.drawable.main_nearby); } navType.setText(res.getString(geo.getLocationProvider().resourceId)); if (geo.getAccuracy() >= 0) { final int speed = Math.round(geo.getSpeed()) * 60 * 60 / 1000; navAccuracy.setText("±" + Units.getDistanceFromMeters(geo.getAccuracy()) + Formatter.SEPARATOR + Units.getSpeed(speed)); } else { navAccuracy.setText(null); } final Geopoint currentCoords = geo.getCoords(); if (Settings.isShowAddress()) { if (addCoords == null) { navLocation.setText(R.string.loc_no_addr); } if (addCoords == null || currentCoords.distanceTo(addCoords) > 0.5) { addCoords = currentCoords; final Single<String> address = (new AndroidGeocoder(MainActivity.this).getFromLocation(currentCoords)).map(new Function<Address, String>() { @Override public String apply(final Address address) { return formatAddress(address); } }).onErrorResumeNext(Single.just(currentCoords.toString())); AndroidRxUtils.bindActivity(MainActivity.this, address) .subscribeOn(AndroidRxUtils.networkScheduler) .subscribe(new Consumer<String>() { @Override public void accept(final String address) { navLocation.setText(address); } }); } } else { navLocation.setText(currentCoords.toString()); } } } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoFindOnMap(final View v) { findOnMap.setPressed(true); startActivity(DefaultMap.getLiveMapIntent(this)); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoFindNearest(final View v) { nearestView.setPressed(true); startActivity(CacheListActivity.getNearestIntent(this)); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoFindByOffline(final View v) { findByOffline.setPressed(true); CacheListActivity.startActivityOffline(this); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoSearch(final View v) { advanced.setPressed(true); startActivity(new Intent(this, SearchActivity.class)); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoPoint(final View v) { any.setPressed(true); startActivity(new Intent(this, NavigateAnyPointActivity.class)); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoFilter(final View v) { filter.setPressed(true); filter.performClick(); } /** * @param v * unused here but needed since this method is referenced from XML layout */ public void cgeoNavSettings(final View v) { startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); } private void checkShowChangelog() { // temporary workaround for #4143 //TODO: understand and avoid if possible try { final long lastChecksum = Settings.getLastChangelogChecksum(); final long checksum = TextUtils.checksum(getString(R.string.changelog_master) + getString(R.string.changelog_release)); Settings.setLastChangelogChecksum(checksum); // don't show change log after new install... if (lastChecksum > 0 && lastChecksum != checksum) { AboutActivity.showChangeLog(this); } } catch (final Exception ex) { Log.e("Error checking/showing changelog!", ex); } } /** * @param view * unused here but needed since this method is referenced from XML layout */ public void showAbout(final View view) { startActivity(new Intent(this, AboutActivity.class)); } @Override public void onBackPressed() { // back may exit the app instead of closing the search action bar if (searchView != null && !searchView.isIconified()) { searchView.setIconified(true); MenuItemCompat.collapseActionView(searchItem); } else { super.onBackPressed(); } } }