diff --git a/src/it/reyboz/bustorino/ActivityMain.java b/src/it/reyboz/bustorino/ActivityMain.java index 4071dfa..98406a1 100644 --- a/src/it/reyboz/bustorino/ActivityMain.java +++ b/src/it/reyboz/bustorino/ActivityMain.java @@ -1,800 +1,796 @@ /* BusTO - Arrival times for Turin public transports. Copyright (C) 2014 Valerio Bozzolan This program 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. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.*; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.NavUtils; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.*; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import android.support.design.widget.FloatingActionButton; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.*; import it.reyboz.bustorino.middleware.*; import java.util.List; import static android.location.Criteria.NO_REQUIREMENT; public class ActivityMain extends GeneralActivity implements FragmentListener { /* * Layout elements */ private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private SwipeRefreshLayout swipeRefreshLayout; private FloatingActionButton floatingActionButton; private FragmentManager framan; private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this (bug #1512948) private int searchMode; /* * Options */ private final String OPTION_SHOW_LEGEND = "show_legend"; private final String LOCATION_PERMISSION_GIVEN = "loc_permission"; /* * Status */ private DBStatusManager prefsManager; private DBStatusManager.OnDBUpdateStatusChangeListener updatelistener; private static final String DEBUG_TAG="BusTO - MainActivity"; /* // useful for testing: public class MockFetcher implements ArrivalsFetcher { @Override public Palina ReadArrivalTimesAll(String routeID, AtomicReference res) { SystemClock.sleep(5000); res.set(result.SERVER_ERROR); return new Palina(); } } private ArrivalsFetcher[] ArrivalFetchers = {new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher(), new MockFetcher()};*/ private RecursionHelper ArrivalFetchersRecursionHelper = new RecursionHelper<>(new ArrivalsFetcher[] {new GTTJSONFetcher(), new FiveTScraperFetcher()}); private RecursionHelper StopsFindersByNameRecursionHelper = new RecursionHelper<>(new StopsFinderByName[] {new GTTStopsFetcher(), new FiveTStopsFetcher()}); /* * Position */ //Fine location criteria private final Criteria cr = new Criteria(); private boolean pendingNearbyStopsRequest = false; private LocationManager locmgr; /* * Database Access */ private StopsDB stopsDB; private UserDB userDB; private FragmentHelper fh; ///////////////////////////////// EVENT HANDLERS /////////////////////////////////////////////// /* * @see swipeRefreshLayout */ private Handler handler = new Handler(); private final Runnable refreshing = new Runnable() { public void run() { if(framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment){ ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); String stopName = fragment.getStopID(); new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(stopName); } else new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); } }; //// MAIN METHOD /// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); framan = getSupportFragmentManager(); this.stopsDB = new StopsDB(getApplicationContext()); this.userDB = new UserDB(getApplicationContext()); setContentView(R.layout.activity_main); busStopSearchByIDEditText = (EditText) findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = (EditText) findViewById(R.id.busStopSearchByNameEditText); progressBar = (ProgressBar) findViewById(R.id.progressBar); howDoesItWorkTextView = (TextView) findViewById(R.id.howDoesItWorkTextView); hideHintButton = (Button) findViewById(R.id.hideHintButton); swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.listRefreshLayout); floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionButton); framan.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() { @Override public void onBackStackChanged() { Log.d("MainActivity, BusTO", "BACK STACK CHANGED"); } }); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); busStopSearchByNameEditText .setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; } }); // Called when the layout is pulled down swipeRefreshLayout .setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { handler.post(refreshing); } }); /** * @author Marco Gagino!!! */ //swipeRefreshLayout.setColorSchemeColors(R.color.blue_500, R.color.orange_500); // setColorScheme is deprecated, setColorSchemeColors isn't swipeRefreshLayout.setColorSchemeResources(R.color.blue_500,R.color.orange_500); fh = new FragmentHelper(this,R.id.listRefreshLayout,R.id.resultFrame); setSearchModeBusStopID(); //---------------------------- START INTENT CHECK QUEUE ------------------------------------ // Intercept calls from URL intent boolean tryedFromIntent = false; String busStopID = null; String busStopDisplayName = null; Uri data = getIntent().getData(); if (data != null) { busStopID = getBusStopIDFromUri(data); tryedFromIntent = true; } // Intercept calls from other activities if (!tryedFromIntent) { Bundle b = getIntent().getExtras(); if (b != null) { busStopID = b.getString("bus-stop-ID"); busStopDisplayName = b.getString("bus-stop-display-name"); /** * I'm not very sure if you are coming from an Intent. * Some launchers work in strange ways. */ tryedFromIntent = busStopID != null; } } //---------------------------- END INTENT CHECK QUEUE -------------------------------------- if (busStopID == null) { // Show keyboard if can't start from intent // JUST DON'T // showKeyboard(); // You haven't obtained anything... from an intent? if (tryedFromIntent) { // This shows a luser warning ArrivalFetchersRecursionHelper.reset(); Toast.makeText(getApplicationContext(), R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show(); } } else { // If you are here an intent has worked successfully setBusStopSearchByIDEditText(busStopID); /* //THIS PART SHOULDN'T BE NECESSARY SINCE THE LAST SUCCESSFULLY SEARCHED BUS // STOP IS ADDED AUTOMATICALLY Stop nextStop = new Stop(busStopID); // forcing it as user name even though it could be standard name, it doesn't really matter nextStop.setStopUserName(busStopDisplayName); //set stop as last succe fh.setLastSuccessfullySearchedBusStop(nextStop); */ createFragmentForStop(busStopID); } //Try (hopefully) database update //TODO: Start the service in foreground, check last time it ran before DatabaseUpdateService.startDBUpdate(getApplicationContext()); /* Set database update */ updatelistener = new DBStatusManager.OnDBUpdateStatusChangeListener(){ @Override public boolean defaultStatusValue() { return true; } @Override public void onDBStatusChanged(boolean updating) { if(updating){ createDefaultSnackbar(); } else if(snackbar!=null){ snackbar.dismiss(); snackbar = null; } } }; prefsManager = new DBStatusManager(getApplicationContext(),updatelistener); prefsManager.registerListener(); //locationHandler = new GPSLocationAdapter(getApplicationContext()); //--------- NEARBY STOPS--------// //SETUP LOCATION locmgr = (LocationManager) getSystemService(LOCATION_SERVICE); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); //We want the nearby bus stops! handler.post(new NearbyStopsRequester()); //If there are no providers available, then, wait for them Log.d("MainActivity", "Created"); } /** * Reload bus stop timetable when it's fulled resumed from background. */ @Override protected void onPostResume() { super.onPostResume(); Log.d("ActivityMain", "onPostResume fired. Last successfully bus stop ID: " + fh.getLastSuccessfullySearchedBusStop()); if (searchMode == SEARCH_BY_ID && fh.getLastSuccessfullySearchedBusStop() != null) { setBusStopSearchByIDEditText(fh.getLastSuccessfullySearchedBusStop().ID); //new asyncWgetBusStopFromBusStopID(lastSuccessfullySearchedBusStop.ID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(); } else { //we have new activity or we don't have a new searched stop. //Let's search stops nearby LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.resultFrame); } //show the FAB since it remains hidden floatingActionButton.show(); - //show the Snackbar //TODO: remove - //snackbar = Snackbar.make(floatingActionButton, - // R.string.database_update_message,Snackbar.LENGTH_INDEFINITE); - //snackbar.show(); } @Override protected void onPause() { super.onPause(); fh.stopLastRequestIfNeeded(); fh.setBlockAllActivities(true); if(updatelistener!=null && prefsManager!=null) prefsManager.unregisterListener(); locmgr.removeUpdates(locListener); } @Override protected void onResume() { super.onResume(); fh.setBlockAllActivities(false); if(updatelistener!=null && prefsManager!=null) { prefsManager.registerListener(); if(prefsManager.isDBUpdating(true)){ createDefaultSnackbar(); } } if(pendingNearbyStopsRequest) handler.post(new NearbyStopsRequester()); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); actionHelpMenuItem = menu.findItem(R.id.action_help); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. switch (item.getItemId()) { case android.R.id.home: // Respond to the action bar's Up/Home button NavUtils.navigateUpFromSameTask(this); return true; case R.id.action_help: showHints(); return true; case R.id.action_favorites: startActivity(new Intent(ActivityMain.this, ActivityFavorites.class)); return true; case R.id.action_about: startActivity(new Intent(ActivityMain.this, ActivityAbout.class)); return true; case R.id.action_news: openIceweasel("https://blog.reyboz.it/tag/busto/"); return true; case R.id.action_bugs: openIceweasel("https://bugs.launchpad.net/bus-torino"); return true; case R.id.action_source: openIceweasel("https://code.launchpad.net/bus-torino"); return true; case R.id.action_licence: openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html"); return true; case R.id.action_author: // TODO: a neutral website with a cute landing page and credits to everyone? -- boz, Thu Jan 17 02:17:45 CET 2019 openIceweasel("https://boz.reyboz.it/?lovebusto"); return true; case R.id.action_settings: Log.d("MAINBusTO","Pressed button preferences"); startActivity(new Intent(ActivityMain.this,ActivitySettings.class)); } return super.onOptionsItemSelected(item); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); //OLD ASYNCTASK //new asyncWgetBusStopFromBusStopID(busStopID, ArrivalFetchersRecursionHelper, lastSuccessfullySearchedBusStop); if(busStopID == null || busStopID.length() <= 0) { showMessage(R.string.insert_bus_stop_number_error); toggleSpinner(false); } else{ new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(busStopID); Log.d("MainActiv","Started search for arrivals of stop "+busStopID); } } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); new AsyncDataDownload(AsyncDataDownload.RequestType.STOPS,fh).execute(query); } } /** PERMISSION STUFF **/ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode){ case PERMISSION_REQUEST_POSITION: if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){ setOption(LOCATION_PERMISSION_GIVEN,true); //if we sent a request for a new NearbyStopsFragment if(pendingNearbyStopsRequest){ pendingNearbyStopsRequest=false; handler.post(new NearbyStopsRequester()); } } else { //permission denied setOption(LOCATION_PERMISSION_GIVEN,false); } //add other cases for permissions } } @Override public void createFragmentForStop(String ID) { //new asyncWgetBusStopFromBusStopID(ID, ArrivalFetchersRecursionHelper,lastSuccessfullySearchedBusStop); if(ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showMessage(R.string.insert_bus_stop_number_error); toggleSpinner(false); } else { new AsyncDataDownload(AsyncDataDownload.RequestType.ARRIVALS,fh).execute(ID); Log.d("MainActiv","Started search for arrivals of stop "+ID); } } /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(this); integrator.initiateScan(); } /** * Receive the Barcode Scanner Intent * */ public void onActivityResult(int requestCode, int resultCode, Intent intent) { IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); Uri uri; try { uri = Uri.parse(scanResult != null ? scanResult.getContents() : null); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { Toast.makeText(getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); createFragmentForStop(busStopID); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } private void createDefaultSnackbar(){ if(snackbar==null){ snackbar = Snackbar.make(findViewById(R.id.searchButton),R.string.database_update_message,Snackbar.LENGTH_INDEFINITE); } snackbar.show(); } ///////////////////////////////// POSITION STUFF////////////////////////////////////////////// private void resolveStopRequest(String provider){ Log.d(DEBUG_TAG,"Provider "+provider+" got enabled"); if(locmgr!=null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)){ pendingNearbyStopsRequest = false; handler.post(new NearbyStopsRequester()); } } final LocationListener locListener = new LocationListener() { @Override public void onLocationChanged(Location location) { Log.d(DEBUG_TAG,"Location changed"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d(DEBUG_TAG,"Location provider status: "+status); if(status== LocationProvider.AVAILABLE){ resolveStopRequest(provider); } } @Override public void onProviderEnabled(String provider) { resolveStopRequest(provider); } @Override public void onProviderDisabled(String provider) { } }; class NearbyStopsRequester implements Runnable{ @SuppressLint("MissingPermission") @Override public void run() { final boolean canRunPosition = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || getOption(LOCATION_PERMISSION_GIVEN, false); if(!canRunPosition){ pendingNearbyStopsRequest = true; assertLocationPermissions(); return; } else setOption(LOCATION_PERMISSION_GIVEN,true); LocationManager locManager = (LocationManager) getSystemService(LOCATION_SERVICE); if(locManager == null) { Log.e(DEBUG_TAG, "location manager is nihil, cannot create NearbyStopsFragment"); return; } if(anyLocationProviderMatchesCriteria(locManager,cr,true) && fh.getLastSuccessfullySearchedBusStop()==null) { //Go ahead with the request Log.d("mainActivity","Recreating stop fragment"); swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = framan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = framan.beginTransaction(); if(oldFrag!=null) ft.remove(oldFrag); ft.add(R.id.resultFrame,fragment,"nearbyStop_correct"); ft.commit(); framan.executePendingTransactions(); pendingNearbyStopsRequest = false; } else if(!anyLocationProviderMatchesCriteria(locManager,cr,true)){ //Wait for the providers Log.d(DEBUG_TAG,"Queuing position request"); pendingNearbyStopsRequest = true; locManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,10,0.1f,locListener); } } } private boolean anyLocationProviderMatchesCriteria(LocationManager mng, Criteria cr, boolean enabled){ List providers = mng.getProviders(cr,enabled); Log.d(DEBUG_TAG,"Getting enabled location providers: "); for(String s: providers){ Log.d(DEBUG_TAG,"Provider "+s); } return providers.size()>0; } ///////////////////////////////// OTHER STUFF ////////////////////////////////////////////////// @Override public void addLastStopToFavorites() { if(fh.getLastSuccessfullySearchedBusStop() != null) { - new AsyncAddToFavorites(this).execute(fh.getLastSuccessfullySearchedBusStop()); + new AsyncAddToFavorites(getApplicationContext()).execute(fh.getLastSuccessfullySearchedBusStop()); } } @Override public void showFloatingActionButton(boolean yes) { if(yes) floatingActionButton.show(); else floatingActionButton.hide(); } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// @Override public void showKeyboard() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } @Override public void showMessage(int messageID) { Toast.makeText(getApplicationContext(), messageID, Toast.LENGTH_SHORT).show(); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } /** * Having that cursor at the left of the edit text makes me cancer. * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); actionHelpMenuItem.setVisible(true); } //TODO: toggle spinner from mainActivity @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); actionHelpMenuItem.setVisible(false); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if(pendingNearbyStopsRequest && (fragmentType==FragmentKind.ARRIVALS || fragmentType==FragmentKind.STOPS)) { locmgr.removeUpdates(locListener); pendingNearbyStopsRequest = false; } if(fragmentType==null) Log.e("ActivityMain","Problem with fragmentType"); else switch (fragmentType){ case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.e("BusTO Activity","Called readyGUI with unsupported type of Fragment"); return; } // Shows hints } /** * Open an URL in the default browser. * * @param url URL */ public void openIceweasel(String url) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(browserIntent1); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if(host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch(host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/middleware/AsyncAddToFavorites.java b/src/it/reyboz/bustorino/middleware/AsyncAddToFavorites.java index 8f040de..746bddd 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncAddToFavorites.java +++ b/src/it/reyboz/bustorino/middleware/AsyncAddToFavorites.java @@ -1,58 +1,58 @@ /* BusTO - Middleware components Copyright (C) 2016 Fabio Mazza This program 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. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.middleware; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.AsyncTask; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.middleware.UserDB; public class AsyncAddToFavorites extends AsyncTask { private Context c; public AsyncAddToFavorites(Context c) { - this.c = c; + this.c = c.getApplicationContext(); } @Override protected Boolean doInBackground(Stop... stops) { - Boolean result; + boolean result; if(stops[0]!=null) { UserDB userDatabase = new UserDB(c); SQLiteDatabase db = userDatabase.getWritableDatabase(); result = UserDB.addOrUpdateStop(stops[0], db); db.close(); } else result = false; return result; } @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); if(result) { Toast.makeText(this.c, R.string.added_in_favorites, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this.c, R.string.cant_add_to_favorites, Toast.LENGTH_SHORT).show(); } } } diff --git a/src/it/reyboz/bustorino/middleware/UserDB.java b/src/it/reyboz/bustorino/middleware/UserDB.java index 8874f91..f038098 100644 --- a/src/it/reyboz/bustorino/middleware/UserDB.java +++ b/src/it/reyboz/bustorino/middleware/UserDB.java @@ -1,248 +1,249 @@ /* BusTO ("backend" components) Copyright (C) 2016 Ludovico Pavesi This program 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. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.middleware; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.content.Context; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.List; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsDBInterface; public class UserDB extends SQLiteOpenHelper { public static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "user.db"; static final String TABLE_NAME = "favorites"; private final Context c; // needed during upgrade private final static String[] usernameColumnNameAsArray = {"username"}; public final static String[] getFavoritesColumnNamesAsArray = {"ID", "username"}; public UserDB(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.c = context; } @Override public void onCreate(SQLiteDatabase db) { // exception intentionally left unhandled db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)"); if(OldDB.doesItExist(this.c)) { upgradeFromOldDatabase(db); } } private void upgradeFromOldDatabase(SQLiteDatabase newdb) { OldDB old; try { old = new OldDB(this.c); } catch(IllegalStateException e) { // can't create database => it doesn't really exist, no matter what doesItExist() says return; } int ver = old.getOldVersion(); /* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app * has crashed midway through the upgrade and the user is retrying, that should never show * up here. And if it does, try to recover favorites anyway. * Versions < 8 already got dropped during the update process, so let's do the same. * * Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it * more complicated. Workaround added in OldDB. */ if(ver >= 8) { ArrayList ID = new ArrayList<>(); ArrayList username = new ArrayList<>(); int len; int len2; try { Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {}); int zero = c.getColumnIndex("busstop_ID"); int one = c.getColumnIndex("busstop_username"); while(c.moveToNext()) { try { ID.add(c.getString(zero)); } catch(Exception e) { // no ID = can't add this continue; } if(c.getString(one) == null || c.getString(one).length() <= 0) { username.add(null); } else { username.add(c.getString(one)); } } c.close(); old.close(); } catch(Exception ignored) { // there's no hope, go ahead and nuke old database. } len = ID.size(); len2 = username.size(); if(len2 < len) { len = len2; } if (len > 0) { try { Stop stopStopStopStopStop; for (int i = 0; i < len; i++) { stopStopStopStopStop = new Stop(ID.get(i)); stopStopStopStopStop.setStopUserName(username.get(i)); addOrUpdateStop(stopStopStopStopStop, newdb); } } catch(Exception ignored) { // partial data is better than no data at all, no transactions here } } } if(!OldDB.destroy(this.c)) { // TODO: notify user somehow? Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user."); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { // nothing to do yet } /** * Gets stop name set by the user. * * @param db readable database * @param stopID stop ID * @return name set by user, or null if not set\not found */ public static String getStopUserName(SQLiteDatabase db, String stopID) { String username = null; try { Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null); if(c.moveToNext()) { username = c.getString(c.getColumnIndex("username")); } c.close(); } catch(SQLiteException ignored) {} return username; } public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) { List l = new ArrayList<>(); Stop s; String stopID, stopUserName; try { Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null); int colID = c.getColumnIndex("ID"); int colUser = c.getColumnIndex("username"); while(c.moveToNext()) { stopUserName = c.getString(colUser); stopID = c.getString(colID); s = dbi.getAllFromID(stopID); if(s == null) { // can't find it in database l.add(new Stop(stopUserName, stopID, null, null, null)); } else { // setStopName() already does sanity checks s.setStopUserName(stopUserName); l.add(s); } } c.close(); } catch(SQLiteException ignored) {} // comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database Collections.sort(l); return l; } public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) { ContentValues cv = new ContentValues(); long result = -1; String un = s.getStopUserName(); cv.put("ID", s.ID); // is there an username? if(un == null) { // no: see if it's in the database cv.put("username", getStopUserName(db, s.ID)); } else { // yes: use it cv.put("username", un); } try { - result = db.insert(TABLE_NAME, null, cv); + //ignore and throw -1 if the row is already in the DB + result = db.insertWithOnConflict(TABLE_NAME, null, cv,SQLiteDatabase.CONFLICT_IGNORE); } catch (SQLiteException ignored) {} // Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return return (result != -1) || updateStop(s, db); } public static boolean updateStop(Stop s, SQLiteDatabase db) { try { ContentValues cv = new ContentValues(); cv.put("username", s.getStopUserName()); db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } public static boolean deleteStop(Stop s, SQLiteDatabase db) { try { db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID}); return true; } catch(SQLiteException e) { return false; } } }