diff --git a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java index 89836af..1b2df98 100644 --- a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java @@ -1,191 +1,191 @@ /* 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.backend; import androidx.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; public class GTTStopsFetcher implements StopsFinderByName { @Override @NonNull public List FindByName(String name, AtomicReference res) { URL url; // sorting an ArrayList should be faster than a LinkedList and the API is limited to 15 results List s = new ArrayList<>(15); List s2 = new ArrayList<>(15); String fullname; String content; String bacino; String localita; Route.Type type; JSONArray json; int howManyStops, i; JSONObject thisstop; if(name.length() < 3) { res.set(Result.QUERY_TOO_SHORT); return s; } try { - url = new URL("http://www.gtt.to.it/cms/components/com_gtt/views/palinejson/view.html.php?term=" + URLEncoder.encode(name, "utf-8")); + url = new URL("https://www.gtt.to.it/cms/index.php?option=com_gtt&view=palinejson&term=" + URLEncoder.encode(name, "utf-8")); } catch (Exception e) { res.set(Result.PARSER_ERROR); return s; } content = networkTools.queryURL(url, res); if(content == null) { return s; } try { json = new JSONArray(content); } catch(JSONException e) { if(content.contains("[]")) { // when no results are found, server returns a PHP Warning and an empty array. In case they fix the warning, we're looking for the array. res.set(Result.EMPTY_RESULT_SET); } else { res.set(Result.PARSER_ERROR); } return s; } howManyStops = json.length(); if(howManyStops == 0) { res.set(Result.EMPTY_RESULT_SET); return s; } try { for(i = 0; i < howManyStops; i++) { thisstop = json.getJSONObject(i); fullname = thisstop.getString("data"); String ID = thisstop.getString("value"); try { localita = thisstop.getString("localita"); if(localita.equals("[MISSING]")) { localita = null; } } catch(JSONException e) { localita = null; } /* if(localita == null || localita.length() == 0) { localita = db.getLocationFromID(ID); } //TODO: find località by ContentProvider */ try { bacino = thisstop.getString("bacino"); } catch (JSONException ignored) { bacino = "U"; } if(fullname.startsWith("Metro ")) { type = Route.Type.METRO; } else if(fullname.length() >= 6 && fullname.startsWith("S00")) { type = Route.Type.RAILWAY; } else if(fullname.startsWith("ST")) { type = Route.Type.RAILWAY; } else { type = FiveTNormalizer.decodeType("", bacino); } //TODO: refactor using content provider s.add(new Stop(fullname, ID, localita, type,null)); } } catch (JSONException e) { res.set(Result.PARSER_ERROR); return s; } if(s.size() < 1) { // shouldn't happen but prevents the next part from catching fire res.set(Result.EMPTY_RESULT_SET); return s; } Collections.sort(s); // the next loop won't work with less than 2 items if(s.size() < 2) { res.set(Result.OK); return s; } /* There are some duplicate stops returned by this API. * Long distance buses have stop IDs with 5 digits. Always. They are zero-padded if there * aren't enough. E.g. stop 631 becomes 00631. * * Unfortunately you can't use padded stops to query any API. * Fortunately, unpadded stops return both normal and long distance bus timetables. * FiveTNormalizer is already removing padding (there may be some padded stops for which the * API doesn't return an unpadded equivalent), here we'll remove duplicates by skipping * padded stops, which also never have a location. * * I had to draw a finite state machine on a piece of paper to understand how to implement * this loop. */ for(i = 1; i < howManyStops; ) { Stop current = s.get(i); Stop previous = s.get(i-1); // same stop: let's see which one to keep... if(current.ID.equals(previous.ID)) { if(previous.location == null) { // previous one is useless: discard it, increment i++; } else if(current.location == null) { // this one is useless: add previous and skip one s2.add(previous); i += 2; } else { // they aren't really identical: to err on the side of caution, keep them both. s2.add(previous); i++; } } else { // different: add previous, increment s2.add(previous); i++; } } // unless the last one was garbage (i would be howManyStops+1 in that case), add it if(i == howManyStops) { s2.add(s.get(i-1)); } res.set(Result.OK); return s2; } } diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java index f7e21cd..af7572f 100644 --- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java +++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java @@ -1,270 +1,271 @@ /* BusTO (fragments) Copyright (C) 2018 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.fragments; import android.content.Context; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import android.util.Log; import android.widget.Toast; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.middleware.*; import java.lang.ref.WeakReference; import java.util.List; /** * Helper class to manage the fragments and their needs */ public class FragmentHelper { //GeneralActivity act; private final FragmentListenerMain listenerMain; private final WeakReference managerWeakRef; private Stop lastSuccessfullySearchedBusStop; //support for multiple frames private final int secondaryFrameLayout; private final int primaryFrameLayout; private final Context context; public static final int NO_FRAME = -3; private static final String DEBUG_TAG = "BusTO FragmHelper"; private WeakReference lastTaskRef; private boolean shouldHaltAllActivities=false; public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) { this(listener,framan, context,mainFrame,NO_FRAME); } public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) { this.listenerMain = listener; this.managerWeakRef = new WeakReference<>(fraMan); this.primaryFrameLayout = primaryFrameLayout; this.secondaryFrameLayout = secondaryFrameLayout; this.context = context.getApplicationContext(); } /** * Get the last successfully searched bus stop or NULL * * @return the stop */ public Stop getLastSuccessfullySearchedBusStop() { return lastSuccessfullySearchedBusStop; } public void setLastSuccessfullySearchedBusStop(Stop stop) { this.lastSuccessfullySearchedBusStop = stop; } public void setLastTaskRef(WeakReference lastTaskRef) { this.lastTaskRef = lastTaskRef; } /** * Called when you need to create a fragment for a specified Palina * @param p the Stop that needs to be displayed */ public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){ boolean sameFragment; ArrivalsFragment arrivalsFragment; if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } FragmentManager fm = managerWeakRef.get(); if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) { arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); //Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?"); assert arrivalsFragment != null; sameFragment = arrivalsFragment.isFragmentForTheSameStop(p); } else { sameFragment = false; Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment"); } setLastSuccessfullySearchedBusStop(p); if(!sameFragment) { //set the String to be displayed on the fragment String displayName = p.getStopDisplayName(); String displayStuff; if (displayName != null && displayName.length() > 0) { arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName); } else { arrivalsFragment = ArrivalsFragment.newInstance(p.ID); } String probableTag = ResultListFragment.getFragmentTag(p); attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack)); } else { Log.d("BusTO", "Same bus stop, accessing existing fragment"); arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout); } // DO NOT CALL `setListAdapter` ever on arrivals fragment arrivalsFragment.updateFragmentData(p); // enable fragment auto refresh arrivalsFragment.setReloadOnResume(true); listenerMain.hideKeyboard(); toggleSpinner(false); } /** * Called when you need to display the results of a search of stops * @param resultList the List of stops found * @param query String queried */ public void createStopListFragment(List resultList, String query, boolean addToBackStack){ listenerMain.hideKeyboard(); StopListFragment listfragment = StopListFragment.newInstance(query); if(managerWeakRef.get()==null || shouldHaltAllActivities) { //SOMETHING WENT VERY WRONG Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything"); return; } attachFragmentToContainer(managerWeakRef.get(),listfragment, new AttachParameters("search_"+query, false,addToBackStack)); listfragment.setStopList(resultList); + listenerMain.readyGUIfor(FragmentKind.STOPS); toggleSpinner(false); } /** * Wrapper for toggleSpinner in Activity * @param on new status of spinner system */ public void toggleSpinner(boolean on){ listenerMain.toggleSpinner(on); } /** * Attach a new fragment to a cointainer * @param fm the FragmentManager * @param fragment the Fragment * @param parameters attach parameters */ protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){ if(shouldHaltAllActivities) //nothing to do return; FragmentTransaction ft = fm.beginTransaction(); int frameID; if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME) // ft.replace(secondaryFrameLayout,fragment,tag); frameID = secondaryFrameLayout; else frameID = primaryFrameLayout; switch (parameters.transaction){ case REPLACE: ft.replace(frameID,fragment,parameters.tag); } if (parameters.addToBackStack) ft.addToBackStack("state_"+parameters.tag); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE); if(!fm.isDestroyed() && !shouldHaltAllActivities) ft.commit(); //fm.executePendingTransactions(); } public synchronized void setBlockAllActivities(boolean shouldI) { this.shouldHaltAllActivities = shouldI; } public void stopLastRequestIfNeeded(){ if(lastTaskRef == null) return; AsyncDataDownload task = lastTaskRef.get(); if(task!=null){ task.cancel(false); } } /** * Wrapper to show the errors/status that happened * @param res result from Fetcher */ public void showErrorMessage(Fetcher.Result res){ //TODO: implement a common set of errors for all fragments switch (res){ case OK: break; case CLIENT_OFFLINE: showToastMessage(R.string.network_error, true); break; case SERVER_ERROR: if (utils.isConnected(context)) { showToastMessage(R.string.parsing_error, true); } else { showToastMessage(R.string.network_error, true); } case PARSER_ERROR: default: showShortToast(R.string.internal_error); break; case QUERY_TOO_SHORT: showShortToast(R.string.query_too_short); break; case EMPTY_RESULT_SET: showShortToast(R.string.no_bus_stop_have_this_name); break; } } public void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; if (context != null) Toast.makeText(context, messageID, length).show(); } private void showShortToast(int messageID){ showToastMessage(messageID, true); } enum Transaction{ REPLACE, } static final class AttachParameters { String tag; boolean attachToSecondaryFrame; Transaction transaction; boolean addToBackStack; public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.transaction = transaction; this.addToBackStack = addToBackStack; } public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) { this.tag = tag; this.attachToSecondaryFrame = attachToSecondaryFrame; this.addToBackStack = addToBackStack; this.transaction = Transaction.REPLACE; } } } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index 50828b5..6f76a9c 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,717 +1,724 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.zxing.integration.android.IntentIntegrator; import java.util.Map; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.middleware.AsyncDataDownload; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{ private static final String OPTION_SHOW_LEGEND = "show_legend"; private static final String SAVED_FRAGMENT="saved_fragment"; private static final String DEBUG_TAG = "BusTO - MainFragment"; public final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private boolean setupOnAttached = true; private boolean suppressArrivalsReload = false; //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 -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; private final ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame); if (fragment == null){ //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncDataDownload(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; private final LocationCriteria cr = new LocationCriteria(2000, 10000); //Location private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() { @Override public void onLocationChanged(Location loc) { } @Override public void onLocationStatusChanged(int status) { if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown()){ //request Stops pendingNearbyStopsRequest = false; if (getContext()!= null) mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @Override public long getLastUpdateTimeMillis() { return 50; } @Override public LocationCriteria getLocationCriteria() { return cr; } @Override public void onLocationProviderAvailable() { //Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest); if(!isNearbyFragmentShown() && getContext()!=null){ pendingNearbyStopsRequest = false; mainHandler.post(new NearbyStopsRequester(getContext(), cr)); } } @Override public void onLocationDisabled() { } }; private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { @Override public void onActivityResult(Map result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) ) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } } } }); //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { MainScreenFragment fragment = new MainScreenFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); locationManager = AppLocationManager.getInstance(getContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Setup on attached: "+setupOnAttached); //Restore instance state if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnAttached = false; } } if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! mainHandler.post(new NearbyStopsRequester(context, cr)); else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnAttached = false; } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called"); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } else if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //we have already asked for the location, and we should show an explanation in order // to ask again (TODO) //do nothing } else{ //request permission requestPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS); } } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } suppressArrivalsReload = false; } if(pendingStopID!=null){ requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); - //new asyncWgetBusStopSuggestions(query, stopsDB, StopsFindersByNameRecursionHelper); - if(getContext()!=null) - new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query); + query = query.trim(); + if(getContext()!=null) { + if (query.length() < 1) { + Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); + } else if(query.length()< 3){ + Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); + } + else + new AsyncDataDownload(fragmentHelper, stopsFinderByNames, getContext()).execute(query); + } } } 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(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } 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); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * 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); } @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); } void showNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); Fragment oldFrag = fragMan.findFragmentById(R.id.resultFrame); FragmentTransaction ft = fragMan.beginTransaction(); if (oldFrag != null) ft.remove(oldFrag); ft.add(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); ft.commit(); } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * 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)) { locationManager.removeLocationRequestFor(requester); 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.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncDataDownload(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncDataDownload(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { new AsyncDataDownload(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java index b3f5b09..bac7eab 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -1,363 +1,382 @@ /* BusTO (middleware) Copyright (C) 2018 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.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.SQLException; import android.net.Uri; import android.os.AsyncTask; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + import android.util.Log; +import android.widget.Toast; +import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.Calendar; /** * This should be used to download data, but not to display it */ public class AsyncDataDownload extends AsyncTask{ private static final String TAG = "BusTO-DataDownload"; private static final String DEBUG_TAG = TAG; private boolean failedAll = false; private final AtomicReference res; private final RequestType t; private String query; WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final Fetcher[] theFetchers; private Context context; private final boolean replaceFragment; public AsyncDataDownload(FragmentHelper fh, @NonNull Fetcher[] fetchers, Context context) { RequestType type; helperRef = new WeakReference<>(fh); fh.setLastTaskRef(new WeakReference<>(this)); res = new AtomicReference<>(); this.context = context.getApplicationContext(); this.replaceFragment = true; theFetchers = fetchers; if (theFetchers.length < 1){ throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); } if (theFetchers[0] instanceof ArrivalsFetcher){ type = RequestType.ARRIVALS; } else if (theFetchers[0] instanceof StopsFinderByName){ type = RequestType.STOPS; } else{ type = null; } t = type; } @Override protected Object doInBackground(String... params) { RecursionHelper r = new RecursionHelper<>(theFetchers); boolean success=false; Object result; FragmentHelper fh = helperRef.get(); + ArrayList results = new ArrayList<>(theFetchers.length); //If the FragmentHelper is null, that means the activity doesn't exist anymore if (fh == null){ return null; } //Log.d(TAG,"refresh layout reference is: "+fh.isRefreshLayoutReferenceTrue()); while(r.valid()) { if(this.isCancelled()) { return null; } //get the data from the fetcher switch (t){ case ARRIVALS: ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); Palina p; String stopID; if(params.length>0) stopID=params[0]; //(it's a Palina) else if(lastSearchedBusStop!=null) stopID = lastSearchedBusStop.ID; //(it's a Palina) else { publishProgress(Fetcher.Result.QUERY_TOO_SHORT); return null; } //Skip the FiveTAPIFetcher for the Metro Stops because it shows incomprehensible arrival times try { if (f instanceof FiveTAPIFetcher && Integer.parseInt(stopID) >= 8200) continue; } catch (NumberFormatException ex){ Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); } p= f.ReadArrivalTimesAll(stopID,res); publishProgress(res.get()); //if (res.get()!= Fetcher.Result.OK) Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+res.get()); if(f instanceof FiveTAPIFetcher){ AtomicReference gres = new AtomicReference<>(); List branches = ((FiveTAPIFetcher) f).getDirectionsForStop(stopID,gres); Log.d(DEBUG_TAG, "FiveTArrivals fetcher: "+f+"\n\tDetails req: "+gres.get()); if(gres.get() == Fetcher.Result.OK){ p.addInfoFromRoutes(branches); Thread t = new Thread(new BranchInserter(branches, context)); t.start(); otherActivities.add(t); } //put updated values into Database } if(lastSearchedBusStop != null && res.get()== Fetcher.Result.OK) { // check that we don't have the same stop if(lastSearchedBusStop.ID.equals(p.ID)) { // searched and it's the same String sn = lastSearchedBusStop.getStopDisplayName(); if(sn != null) { // "merge" Stop over Palina and we're good to go p.mergeNameFrom(lastSearchedBusStop); } } } p.mergeDuplicateRoutes(0); if(p.queryAllRoutes().size() == 0) //skip the rest and go to the next fetcher continue; result = p; //TODO: find a way to avoid overloading the user with toasts break; case STOPS: StopsFinderByName finder = (StopsFinderByName) r.getAndMoveForward(); List resultList= finder.FindByName(params[0], this.res); //it's a List Log.d(TAG,"Using the StopFinderByName: "+finder.getClass()); query =params[0]; result = resultList; //dummy result + Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultList.size()+" stops"); break; default: result = null; } //find if it went well + results.add(res.get()); if(res.get()== Fetcher.Result.OK) { //wait for other threads to finish for(Thread t: otherActivities){ try { t.join(); } catch (InterruptedException e) { //do nothing } } return result; } } + boolean emptyResults = true; + for (Fetcher.Result re: results){ + if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { + emptyResults = false; + break; + } + } + if(emptyResults){ + if(t==RequestType.STOPS) + publishProgress(Fetcher.Result.EMPTY_RESULT_SET); + } //at this point, we are sure that the result has been negative failedAll=true; return null; } @Override protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperRef.get(); if (fh!=null) for (Fetcher.Result r : values){ //TODO: make Toast fh.showErrorMessage(r); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onPostExecute(Object o) { FragmentHelper fh = helperRef.get(); if(failedAll || o == null || fh == null){ //everything went bad if(fh!=null) fh.toggleSpinner(false); cancel(true); //TODO: send message here return; } if(isCancelled()) return; switch (t){ case ARRIVALS: Palina palina = (Palina) o; fh.createOrUpdateStopFragment(palina, replaceFragment); break; case STOPS: //this should never be a problem if(!(o instanceof List)){ throw new IllegalStateException(); } List list = (List) o; if (list.size() ==0) return; Object firstItem = list.get(0); if(!(firstItem instanceof Stop)) return; ArrayList stops = new ArrayList<>(); for(Object x: list){ if(x instanceof Stop) stops.add((Stop) x); + //Log.d(DEBUG_TAG, "Parsing Stop: "+x); } if(list.size() != stops.size()){ Log.w(DEBUG_TAG, "Wrong stop list size:\n incoming: "+ list.size()+" out: "+stops.size()); } //List stopList = (List) list; if(query!=null && !isCancelled()) { fh.createStopListFragment(stops,query, replaceFragment); } else Log.e(TAG,"QUERY NULL, COULD NOT CREATE FRAGMENT"); break; case DBUPDATE: break; } } @Override protected void onCancelled() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperRef.get(); if (fh!=null) fh.toggleSpinner(true); } public enum RequestType { ARRIVALS,STOPS,DBUPDATE } public static class BranchInserter implements Runnable{ private final List routesToInsert; private final Context context; //private final NextGenDB nextGenDB; public BranchInserter(List routesToInsert,@NonNull Context con) { this.routesToInsert = routesToInsert; this.context = con.getApplicationContext(); //nextGenDB = new NextGenDB(context); } @Override public void run() { final NextGenDB nextGenDB = new NextGenDB(context); //ContentValues[] values = new ContentValues[routesToInsert.size()]; ArrayList branchesValues = new ArrayList<>(routesToInsert.size()*4); ArrayList connectionsVals = new ArrayList<>(routesToInsert.size()*4); long starttime,endtime; for (Route r:routesToInsert){ //if it has received an interrupt, stop if(Thread.interrupted()) return; //otherwise, build contentValues final ContentValues cv = new ContentValues(); cv.put(BranchesTable.COL_BRANCHID,r.branchid); cv.put(LinesTable.COLUMN_NAME,r.getName()); cv.put(BranchesTable.COL_DIRECTION,r.destinazione); cv.put(BranchesTable.COL_DESCRIPTION,r.description); for (int day :r.serviceDays) { switch (day){ case Calendar.MONDAY: cv.put(BranchesTable.COL_LUN,1); break; case Calendar.TUESDAY: cv.put(BranchesTable.COL_MAR,1); break; case Calendar.WEDNESDAY: cv.put(BranchesTable.COL_MER,1); break; case Calendar.THURSDAY: cv.put(BranchesTable.COL_GIO,1); break; case Calendar.FRIDAY: cv.put(BranchesTable.COL_VEN,1); break; case Calendar.SATURDAY: cv.put(BranchesTable.COL_SAB,1); break; case Calendar.SUNDAY: cv.put(BranchesTable.COL_DOM,1); break; } } if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode()); cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode()); //values[routesToInsert.indexOf(r)] = cv; branchesValues.add(cv); if(r.getStopsList() != null) for(int i=0; i0) { starttime = System.currentTimeMillis(); ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]); Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections"); int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME); endtime = System.currentTimeMillis(); Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows"); } nextGenDB.close(); } } }