diff --git a/app/build.gradle b/app/build.gradle index 8fa8951..391bf48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,135 +1,134 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { compileSdkVersion 33 buildToolsVersion '33.0.2' namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 21 targetSdkVersion 33 versionCode 48 versionName "1.19.1" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } lintOptions { abortOnError false } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries - } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.9.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.17.2' // mqtt library implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'com.github.hannesa2:paho.mqtt.android:3.5.3' // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 077f756..5d81892 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,125 +1,122 @@ - + - - + + - - + + - + - + - + android:windowSoftInputMode="adjustResize"> - \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index cd051f2..83872f2 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,294 +1,294 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.util.RoutePositionSorter; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; - private @Nullable Location userPosition; + private @NonNull GPSPoint userPosition; private FragmentListenerMain listener; private List< Pair > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable private String KEY_CAPITALIZE; private NameCapitalize capit; - public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @Nullable Location pos) { + public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @NonNull GPSPoint pos) { listener = fragmentListener; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); this.capit = NameCapitalize.getCapitalize(capitalizeKey); } @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS final Pair stopRoutePair = routesPairList.get(position); if(stopRoutePair!=null && stopRoutePair.first!=null){ final Stop stop = stopRoutePair.first; final Route r = stopRoutePair.second; - final Double distance = stop.getDistanceFromLocation(userPosition); + final Double distance = stop.getDistanceFromLocation(userPosition.getLatitude(), userPosition.longitude); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { holder.lineNameTextView.setText(r.getNameForDisplay()); holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return routesPairList.size(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); capit = NameCapitalize.getCapitalize(k); notifyDataSetChanged(); } } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } } - public void setUserPosition(@Nullable Location userPosition) { + public void setUserPosition(@Nullable GPSPoint userPosition) { this.userPosition = userPosition; } - public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable Location pos) { + public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable GPSPoint pos) { if(pos!=null){ this.userPosition = pos; } if(mRoutesPairList!=null){ //this.routesPairList = routesPairList; //remove duplicates sortAndRemoveDuplicates(mRoutesPairList, this.userPosition); //routesPairList = mRoutesPairList; //STUPID CODE if (this.routesPairList == null || routesPairList.size() == 0){ routesPairList = mRoutesPairList; notifyDataSetChanged(); } else{ final HashMap, Integer> indexMapIn = getRouteIndexMap(mRoutesPairList); final HashMap, Integer> indexMapExisting = getRouteIndexMap(routesPairList); //List> oldList = routesPairList; routesPairList = mRoutesPairList; /* for (Pair pair: indexMapIn.keySet()){ final Integer posIn = indexMapIn.get(pair); if (posIn == null) continue; if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemMoved(posExisting, posIn); indexMapExisting.remove(pair); } else{ //INSERT IT //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemInserted(posIn); } }// //REMOVE OLD STOPS for (Pair pair: indexMapExisting.keySet()) { final Integer posExisting = indexMapExisting.get(pair); if (posExisting == null) continue; //routesPairList.remove(posExisting.intValue()); notifyItemRemoved(posExisting); } //*/notifyDataSetChanged(); } //remove and join the } } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } /** * Sort and remove the repetitions in the list */ - private static void sortAndRemoveDuplicates(List< Pair > routesPairList, Location positionToSort ){ + private static void sortAndRemoveDuplicates(List< Pair > routesPairList, GPSPoint positionToSort ){ Collections.sort(routesPairList,new RoutePositionSorter(positionToSort)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } private static HashMap, Integer> getRouteIndexMap(List> routesPairList){ final HashMap, Integer> myMap = new HashMap<>(); for (int i=0; i(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); } return myMap; } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java index 82e042b..b7e194c 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/SquareStopAdapter.java @@ -1,128 +1,129 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.location.Location; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; +import it.reyboz.bustorino.backend.GPSPoint; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.util.StopSorterByDistance; import it.reyboz.bustorino.fragments.FragmentListenerMain; import java.util.Collections; import java.util.List; public class SquareStopAdapter extends RecyclerView.Adapter { private final static int layoutRes = R.layout.stop_card; //private List stops; - private @Nullable Location userPosition; + private @Nullable GPSPoint userPosition; private FragmentListenerMain listener; private List stops; - public SquareStopAdapter(@Nullable List stopList, FragmentListenerMain fragmentListener, @Nullable Location pos) { + public SquareStopAdapter(@Nullable List stopList, FragmentListenerMain fragmentListener, @Nullable GPSPoint pos) { listener = fragmentListener; userPosition = pos; stops = stopList; } @Override public SquareViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); //sort the stops by distance if(stops != null && stops.size() > 0) Collections.sort(stops,new StopSorterByDistance(userPosition)); return new SquareViewHolder(view); } @Override public void onBindViewHolder(SquareViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(stops==null || stops.size() == 0) return; //NO STOPS final Stop stop = stops.get(position); if(stop!=null){ if(stop.getDistanceFromLocation(userPosition)!=Double.POSITIVE_INFINITY){ Double distance = stop.getDistanceFromLocation(userPosition); holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } holder.stopNameView.setText(stop.getStopDisplayName()); holder.stopIDView.setText(stop.ID); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { holder.routesView.setVisibility(View.GONE); } else { holder.routesView.setText(whatStopsHere); holder.routesView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return stops.size(); } class SquareViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView stopIDView; TextView stopNameView; TextView routesView; TextView distancetextView; String stopID; SquareViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); stopIDView = (TextView) holdView.findViewById(R.id.stop_numberText); stopNameView = (TextView) holdView.findViewById(R.id.stop_nameText); routesView = (TextView) holdView.findViewById(R.id.stop_linesText); distancetextView = (TextView) holdView.findViewById(R.id.stop_distanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void setStops(List stops) { this.stops = stops; } - public void setUserPosition(@Nullable Location userPosition) { + public void setUserPosition(@Nullable GPSPoint userPosition) { this.userPosition = userPosition; } /* @Override public Stop getItem(int position) { return stops.get(position); } */ } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/GPSPoint.java b/app/src/main/java/it/reyboz/bustorino/backend/GPSPoint.java new file mode 100644 index 0000000..df3c74b --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/backend/GPSPoint.java @@ -0,0 +1,34 @@ +package it.reyboz.bustorino.backend; + +import org.osmdroid.api.IGeoPoint; + +public class GPSPoint implements IGeoPoint { + + public final double latitude; + public final double longitude; + + public GPSPoint(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + @Override + public int getLatitudeE6() { + return (int) (latitude*1e6d); + } + + @Override + public int getLongitudeE6() { + return (int) (longitude*1e6d); + } + + @Override + public double getLatitude() { + return latitude; + } + + @Override + public double getLongitude() { + return longitude; + } +} diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java index b282856..a2da413 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java @@ -1,305 +1,308 @@ /* 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 android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.reyboz.bustorino.util.LinesNameSorter; +import org.osmdroid.api.IGeoPoint; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import java.util.Locale; public class Stop implements Comparable { // remove "final" in case you need to set these from outside the parser\scrapers\fetchers public final @NonNull String ID; private @Nullable String name; private @Nullable String username; public @Nullable String location; public @Nullable Route.Type type; private @Nullable List routesThatStopHere; private final @Nullable Double lat; private final @Nullable Double lon; // leave this non-final private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; // public @Nullable String gtfsID = null; /** * Hey, look, method overloading! */ public Stop(final @Nullable String name, final @NonNull String ID, @Nullable final String location, @Nullable final Route.Type type, @Nullable final List routesThatStopHere) { this.ID = ID; this.name = name; this.username = null; this.location = (location != null && location.length() == 0) ? null : location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = null; this.lon = null; } /** * Hey, look, method overloading! */ public Stop(final @NonNull String ID) { this.ID = ID; this.name = null; this.username = null; this.location = null; this.type = null; this.routesThatStopHere = null; this.lat = null; this.lon = null; } /** * Constructor that sets EVERYTHING. */ public Stop(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { this.ID = ID; this.name = name; this.username = userName; this.location = location; this.type = type; this.routesThatStopHere = routesThatStopHere; this.lat = lat; this.lon = lon; this.gtfsID = gtfsID; } public @Nullable String routesThatStopHereToString() { // M E M O I Z A T I O N if(this.routesThatStopHereString != null) { return this.routesThatStopHereString; } // no string yet? build it! return buildRoutesString(); } @Nullable public String getAbsurdGTTPlaceName() { return absurdGTTPlaceName; } public void setAbsurdGTTPlaceName(@NonNull String absurdGTTPlaceName) { this.absurdGTTPlaceName = absurdGTTPlaceName; } public void setRoutesThatStopHere(@Nullable List routesThatStopHere) { this.routesThatStopHere = routesThatStopHere; } protected void setRoutesThatStopHereString(String routesStopping){ this.routesThatStopHereString = routesStopping; } @Nullable protected List getRoutesThatStopHere(){ return routesThatStopHere; } protected @Nullable String buildRoutesString() { // no routes => no string if(this.routesThatStopHere == null || this.routesThatStopHere.size() == 0) { return null; } StringBuilder sb = new StringBuilder(); Collections.sort(routesThatStopHere,new LinesNameSorter()); int i, lenMinusOne = routesThatStopHere.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routesThatStopHere.get(i)).append(", "); } // last one: sb.append(routesThatStopHere.get(i)); this.routesThatStopHereString = sb.toString(); return this.routesThatStopHereString; } @Override public int compareTo(@NonNull Stop other) { int res; int thisAsInt = networkTools.failsafeParseInt(this.ID); int otherAsInt = networkTools.failsafeParseInt(other.ID); // numeric stop IDs if(thisAsInt != 0 && otherAsInt != 0) { return thisAsInt - otherAsInt; } else { // non-numeric res = this.ID.compareTo(other.ID); if (res != 0) { return res; } } // try with name, then if(this.name != null && other.name != null) { res = this.name.compareTo(other.name); } // and give up return res; } /** * Sets a name. * * @param name stop name as string (not null) */ public final void setStopName(@NonNull String name) { this.name = name; } /** * Sets user name. Empty string is converted to null. * * @param name a string of non-zero length, or null */ public final void setStopUserName(@Nullable String name) { if(name == null) { this.username = null; } else if(name.length() == 0) { this.username = null; } else { this.username = name; } } /** * Returns stop name or username (if set).
* - empty string means "already searched everywhere, can't find it"
* - null means "didn't search, yet. Maybe you should try."
* - string means "here's the name.", obviously.
* * @return string if known, null if still unknown */ public final @Nullable String getStopDisplayName() { if(this.username == null) { return this.name; } else { return this.username; } } /** * Same as getStopDisplayName, only returns default name.
* I'd use an @see tag, but Android Studio is incapable of understanding that getStopDefaultName * refers to the method exactly above this one and not some arcane and esoteric unknown symbol. */ public final @Nullable String getStopDefaultName() { return this.name; } /** * Same as getStopDisplayName, only returns user name.
* Also, never an empty string. */ public final @Nullable String getStopUserName() { return this.username; } /** * Gets username and name from other stop if they exist, sets itself accordingly. * * @param other another Stop * @return did we actually set/change anything? */ public final boolean mergeNameFrom(Stop other) { boolean ret = false; if(other.name != null) { if(this.name == null || !this.name.equals(other.name)) { this.name = other.name; ret = true; } } if(other.username != null) { if(this.username == null || !this.username.equals(other.username)) { this.username = other.username; ret = true; } } return ret; } public final @Nullable String getGeoURL() { if(this.lat == null || this.lon == null) { return null; } // Android documentation suggests US for machine readable output (use dot as decimal separator) return String.format(Locale.US, "geo:%f,%f", this.lat, this.lon); } public final @Nullable String getGeoURLWithAddress() { String url = getGeoURL(); if(url == null) { return null; } if(this.location != null) { try { String addThis = "?q=".concat(URLEncoder.encode(this.location, "utf-8")); return url.concat(addThis); } catch (Exception ignored) {} } return url; } @Nullable public Double getLatitude() { return lat; } @Nullable public Double getLongitude() { return lon; } - public Double getDistanceFromLocation(Location loc){ + public Double getDistanceFromLocation(IGeoPoint loc){ + return getDistanceFromLocation(loc.getLatitude(), loc.getLongitude()); + } + public Double getDistanceFromLocation(double latitude, double longitude){ if(this.lat!=null && this.lon !=null) - return utils.measuredistanceBetween(this.lat,this.lon,loc.getLatitude(),loc.getLongitude()); + return utils.measuredistanceBetween(this.lat,this.lon,latitude, longitude); else return Double.POSITIVE_INFINITY; - } -} + }} diff --git a/app/src/main/java/it/reyboz/bustorino/backend/utils.java b/app/src/main/java/it/reyboz/bustorino/backend/utils.java index 55e6755..00efda3 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/utils.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/utils.java @@ -1,356 +1,375 @@ /* BusTO (backend components) Copyright (C) 2019 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.backend; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; -import android.view.View; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.fragments.SettingsFragment; public abstract class utils { - private static final double EarthRadius = 6371e3; + private static final double EARTH_RADIUS = 6371.009e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); - return Math.abs(EarthRadius*c); + return Math.abs(EARTH_RADIUS *c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ - return Math.toDegrees(distanceInMeters/EarthRadius); + return Math.toDegrees(distanceInMeters/ EARTH_RADIUS); } public static int convertDipToPixelsInt(Context con,double dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } + /** + * Convert distance in meters on Earth in degrees of latitude, keeping the same longitude + * @param distanceMeters distance in meters + * @return angle in degrees + */ + public static Double latitudeDelta(Double distanceMeters){ + final double angleRad = distanceMeters/EARTH_RADIUS; + return Math.toDegrees(angleRad); + } + + /** + * Convert distance in meters on Earth in degrees of longitude, keeping the same latitude + * @param distanceMeters distance in meters + * @param latitude the latitude that is fixed + * @return angle in degrees + */ + public static Double longitudeDelta(Double distanceMeters, Double latitude){ + final double theta = Math.toRadians(latitude); + final double denom = Math.abs(Math.cos(theta)); + final double angleRad = 2*Math.asin(Math.sin(distanceMeters / EARTH_RADIUS) / denom); + return Math.toDegrees(angleRad); + } public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// 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; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } //SPLIT ON ', check if contains "D'" if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ sb.append("D'"); subs = subs.substring(2); } int index = 0; char c = subs.charAt(index); if(subs.length() > 1 && c=='('){ sb.append(c); index += 1; c = subs.charAt(index); } sb.append(Character.toUpperCase(c)); if (lowercaseRest) sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else sb.append(subs.substring(index+1)); } if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Get the default list of fetchers for arrival times * @return array of ArrivalsFetchers to use */ public static List getDefaultArrivalsFetchers(Context context){ SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); final Set setSelected = new HashSet<>(defSharPref.getStringSet(SettingsFragment.KEY_ARRIVALS_FETCHERS_USE, new HashSet<>())); if (setSelected.isEmpty()) { return Arrays.asList(new MatoAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()); }else{ ArrayList outFetchers = new ArrayList<>(4); /*for(String s: setSelected){ switch (s){ case "matofetcher": outFetchers.add(new MatoAPIFetcher()); break; case "fivetapifetcher": outFetchers.add(new FiveTAPIFetcher()); break; case "gttjsonfetcher": outFetchers.add(new GTTJSONFetcher()); break; case "fivetscraper": outFetchers.add(new FiveTScraperFetcher()); break; default: throw new IllegalArgumentException(); } }*/ if (setSelected.contains("matofetcher")) { outFetchers.add(new MatoAPIFetcher()); setSelected.remove("matofetcher"); } if (setSelected.contains("fivetapifetcher")) { outFetchers.add(new FiveTAPIFetcher()); setSelected.remove("fivetapifetcher"); } if (setSelected.contains("gttjsonfetcher")){ outFetchers.add(new GTTJSONFetcher()); setSelected.remove("gttjsonfetcher"); } if (setSelected.contains("fivetscraper")) { outFetchers.add(new FiveTScraperFetcher()); setSelected.remove("fivetscraper"); } if(!setSelected.isEmpty()){ Log.e("BusTO-Utils","Getting some fetchers values which are not contemplated: "+setSelected); } return outFetchers; } } /*public String getShorterDirection(String headSign){ String[] parts = headSign.split(","); if (parts.length<=1){ return headSign.trim(); } String first = parts[0].trim(); String second = parts[1].trim(); String firstLower = first.toLowerCase(Locale.ITALIAN); switch (firstLower){ case "circolare destra": case "circolare sinistra": case } }*/ /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i Set convertArrayToSet(T[] array) { // Create an empty Set Set set = new HashSet<>(); // Add each element into the set set.addAll(Arrays.asList(array)); // Return the converted Set return set; } public static String giveClassesForArray(T[] array){ StringBuilder sb = new StringBuilder(); for (T f: array){ sb.append(""); sb.append(f.getClass().getSimpleName()); sb.append("; "); } return sb.toString(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java b/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java index 41709b7..dde6b78 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java +++ b/app/src/main/java/it/reyboz/bustorino/data/AppDataProvider.java @@ -1,283 +1,288 @@ /* 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.data; import android.content.*; import android.database.Cursor; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.util.Log; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.NextGenDB.Contract.*; import java.util.List; import static it.reyboz.bustorino.data.UserDB.getFavoritesColumnNamesAsArray; public class AppDataProvider extends ContentProvider { public static final String AUTHORITY = BuildConfig.APPLICATION_ID +".provider"; private static final int STOP_OP = 1; private static final int LINE_OP = 2; private static final int BRANCH_OP = 3; private static final int FAVORITES_OP =4; private static final int MANY_STOPS = 5; private static final int ADD_UPDATE_BRANCHES = 6; private static final int LINE_INSERT_OP = 7; private static final int CONNECTIONS = 8; private static final int LOCATION_SEARCH = 9; private static final int GET_ALL_FAVORITES =10; public static final String FAVORITES = "favorites"; private static final String DEBUG_TAG="AppDataProvider"; private Context con; private NextGenDB appDBHelper; private UserDB userDBHelper; private SQLiteDatabase db; private DBStatusManager preferences; public AppDataProvider() { } private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. */ sUriMatcher.addURI(AUTHORITY, "stop/#", STOP_OP); sUriMatcher.addURI(AUTHORITY,"stops",MANY_STOPS); sUriMatcher.addURI(AUTHORITY,"stops/location/*/*/*",LOCATION_SEARCH); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ sUriMatcher.addURI(AUTHORITY, "line/#", LINE_OP); sUriMatcher.addURI(AUTHORITY,"branch/#",BRANCH_OP); sUriMatcher.addURI(AUTHORITY,"line/insert",LINE_INSERT_OP); sUriMatcher.addURI(AUTHORITY,"branches",ADD_UPDATE_BRANCHES); sUriMatcher.addURI(AUTHORITY,"connections",CONNECTIONS); sUriMatcher.addURI(AUTHORITY,"favorites/#",FAVORITES_OP); sUriMatcher.addURI(AUTHORITY,FAVORITES,GET_ALL_FAVORITES); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Implement this to handle requests to delete one or more rows. db = appDBHelper.getWritableDatabase(); int rows; switch (sUriMatcher.match(uri)){ case MANY_STOPS: rows = db.delete(NextGenDB.Contract.StopsTable.TABLE_NAME,null,null); break; default: throw new UnsupportedOperationException("Not yet implemented"); } return rows; } @Override public String getType(Uri uri) { // TODO: Implement this to handle requests for the MIME type of the data // at the given URI. int match = sUriMatcher.match(uri); String baseTypedir = "vnd.android.cursor.dir/"; String baseTypeitem = "vnd.android.cursor.item/"; switch (match){ case LOCATION_SEARCH: return baseTypedir+"stop"; case LINE_OP: return baseTypeitem+"line"; case CONNECTIONS: return baseTypedir+"stops"; } return baseTypedir+"/item"; } @Override public Uri insert(Uri uri, ContentValues values) throws IllegalArgumentException{ //AVOID OPENING A DB CONNECTION, WILL THROW VERY NASTY ERRORS if(preferences.isDBUpdating(true)) return null; db = appDBHelper.getWritableDatabase(); Uri finalUri; long last_rowid = -1; switch (sUriMatcher.match(uri)){ case ADD_UPDATE_BRANCHES: Log.d("InsBranchWithProvider","new Insert request"); String line_name = values.getAsString(NextGenDB.Contract.LinesTable.COLUMN_NAME); if(line_name==null) throw new IllegalArgumentException("No line name given"); long lineid = -1; Cursor c = db.query(LinesTable.TABLE_NAME, new String[]{LinesTable._ID,LinesTable.COLUMN_NAME,LinesTable.COLUMN_DESCRIPTION},NextGenDB.Contract.LinesTable.COLUMN_NAME +" =?", new String[]{line_name},null,null,null); Log.d("InsBranchWithProvider","finding line in the database: "+c.getCount()+" matches"); if(c.getCount() == 0){ //There are no lines, insert? //NOPE /* c.close(); ContentValues cv = new ContentValues(); cv.put(LinesTable.COLUMN_NAME,line_name); lineid = db.insert(LinesTable.TABLE_NAME,null,cv); */ break; }else { c.moveToFirst(); /* while(c.moveToNext()){ Log.d("InsBranchWithProvider","line: "+c.getString(c.getColumnIndex(LinesTable.COLUMN_NAME))+"\n" +c.getString(c.getColumnIndex(LinesTable.COLUMN_DESCRIPTION))); }*/ lineid = c.getInt(c.getColumnIndexOrThrow(NextGenDB.Contract.LinesTable._ID)); c.close(); } values.remove(NextGenDB.Contract.LinesTable.COLUMN_NAME); values.put(BranchesTable.COL_LINE,lineid); last_rowid = db.insertWithOnConflict(NextGenDB.Contract.BranchesTable.TABLE_NAME,null,values,SQLiteDatabase.CONFLICT_REPLACE); break; case MANY_STOPS: //Log.d("AppDataProvider_busTO","New stop insert request"); try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.StopsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; case CONNECTIONS: try{ last_rowid = db.insertOrThrow(NextGenDB.Contract.ConnectionsTable.TABLE_NAME,null,values); } catch (SQLiteConstraintException e){ Log.w("AppDataProvider_busTO","Insert failed because of constraint"); last_rowid = -1; e.printStackTrace(); } break; default: throw new IllegalArgumentException("Invalid parameters"); } finalUri = ContentUris.withAppendedId(uri,last_rowid); return finalUri; } @Override public boolean onCreate() { con = getContext(); appDBHelper = NextGenDB.getInstance(getContext()); userDBHelper = new UserDB(getContext()); if(con!=null) { preferences = new DBStatusManager(con,null); } else { preferences = null; Log.e(DEBUG_TAG,"Cannot get shared preferences"); } return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws UnsupportedOperationException,IllegalArgumentException { //IMPORTANT //The app should not query when the DB is updating, but apparently, it does if(preferences.isDBUpdating(true)) //throw new UnsupportedOperationException("DB is updating"); return null; SQLiteDatabase db = appDBHelper.getReadableDatabase(); List parts = uri.getPathSegments(); switch (sUriMatcher.match(uri)){ case LOCATION_SEARCH: //authority/stops/location/"Lat"/"Lon"/"distance" //distance in metres (integer) if(parts.size()>=4 && "location".equals(parts.get(1))){ Double latitude = Double.parseDouble(parts.get(2)); Double longitude = Double.parseDouble(parts.get(3)); - //converting distance to a float to not lose precision - float distance = parts.size()>=5 ? Float.parseFloat(parts.get(4))/1000 : 0.02f; + //distance in meters + final double distance = parts.size()>=5 ? Double.parseDouble(parts.get(4)) : 50; //if(parts.size()>=5) //Log.d("LocationSearch"," given distance to search is "+parts.get(4)+" m"); - Double distasAngle = (distance/6371)*180/Math.PI; //small angles approximation, still valid for about 500 metres + Double latDelta = utils.latitudeDelta(distance); + Double longDelta = utils.longitudeDelta(distance, latitude); + Log.d(DEBUG_TAG, "Location search around: "+latitude+" , "+longitude); + Log.d(DEBUG_TAG, "Location search: latitude {"+(latitude-latDelta)+", "+(latitude+latDelta)+ + "} longitude {"+(longitude-longDelta)+", "+(longitude+longDelta)+"}"); - String whereClause = StopsTable.COL_LAT+ "< "+(latitude+distasAngle)+" AND " - +StopsTable.COL_LAT +" > "+(latitude-distasAngle)+" AND "+ - StopsTable.COL_LONG+" < "+(longitude+distasAngle)+" AND "+StopsTable.COL_LONG+" > "+(longitude-distasAngle); + String whereClause = StopsTable.COL_LAT+ "< "+(latitude+latDelta)+" AND " + +StopsTable.COL_LAT +" > "+(latitude-latDelta)+" AND "+ + StopsTable.COL_LONG+" < "+(longitude+longDelta)+" AND "+StopsTable.COL_LONG+" > "+(longitude-longDelta); //Log.d("Provider-LOCSearch","Querying stops by position, query args: \n"+whereClause); return db.query(StopsTable.TABLE_NAME,projection,whereClause,null,null,null,null); } else { Log.w(DEBUG_TAG,"Not enough parameters"); if(parts.size()>=5) for(String s:parts) Log.d(DEBUG_TAG,"\t element "+parts.indexOf(s)+" is: "+s); return null; } case FAVORITES_OP: final String stopFavSelection = getFavoritesColumnNamesAsArray[0]+" = ?"; db = userDBHelper.getReadableDatabase(); Log.d(DEBUG_TAG,"Asked information on Favorites about stop with id "+uri.getLastPathSegment()); return db.query(UserDB.TABLE_NAME,projection,stopFavSelection,new String[]{uri.getLastPathSegment()},null,null,sortOrder); case STOP_OP: //Let's try this plain and simple final String[] selectionValues = {uri.getLastPathSegment()}; final String stopSelection = StopsTable.COL_ID+" = ?"; Log.d(DEBUG_TAG,"Asked information about stop with id "+selectionValues[0]); return db.query(StopsTable.TABLE_NAME,projection,stopSelection,selectionValues,null,null,sortOrder); case MANY_STOPS: return db.query(StopsTable.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); case GET_ALL_FAVORITES: db = userDBHelper.getReadableDatabase(); return db.query(UserDB.TABLE_NAME, projection, selection, selectionArgs, null, null,sortOrder); default: Log.e("DataProvider","got request "+uri.getPath()+" which doesn't match anything"); } throw new UnsupportedOperationException("Not yet implemented"); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO: Implement this to handle requests to update one or more rows. throw new UnsupportedOperationException("Not yet implemented"); } // public static Uri getBaseUriGivenOp(int operationType); public static Uri.Builder getUriBuilderToComplete(){ final Uri.Builder b = new Uri.Builder(); b.scheme("content").authority(AUTHORITY); return b; } @Override public void onLowMemory() { super.onLowMemory(); } } diff --git a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt index 380b67d..e4defb3 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt @@ -1,69 +1,84 @@ /* BusTO - Data components Copyright (C) 2021 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.data import android.content.Context import it.reyboz.bustorino.backend.Result import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.backend.utils +import java.util.ArrayList import java.util.concurrent.Executor class OldDataRepository(private val executor: Executor, private val nextGenDB: NextGenDB) { constructor(executor: Executor, context: Context): this(executor, NextGenDB.getInstance(context)) fun requestStopsWithGtfsIDs( gtfsIDs: List?, callback: Callback> ) { executor.execute { try { //final NextGenDB dbHelper = new NextGenDB(context); val db = nextGenDB.readableDatabase val stops: List = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs) //Result> result = Result.success; callback.onComplete(Result.success(stops)) } catch (e: Exception) { callback.onComplete(Result.failure(e)) } } } fun requestStopsInArea( latitFrom: Double, latitTo: Double, longitFrom: Double, longitTo: Double, callback: Callback> ){ //Log.d(DEBUG_TAG, "Async Stop Fetcher started working"); executor.execute { val stops = nextGenDB.queryAllInsideMapView( latitFrom, latitTo, longitFrom, longitTo ) - if (stops!=null) - callback.onComplete(Result.success(stops)) + + callback.onComplete(Result.success(stops)) } } + /** + * Request all the stops in position [latitude], [longitude], in the "square" with radius [distanceMeters] + * Returns nothing, [callback] will be called if the query succeeds + */ + fun requestStopsWithinDistance(latitude: Double, longitude: Double, distanceMeters: Int, callback: Callback>){ + + val latDelta = utils.latitudeDelta(distanceMeters.toDouble()) + val longDelta = utils.longitudeDelta(distanceMeters.toDouble(), latitude) + + requestStopsInArea(latitude-latDelta, + latitude+latDelta, longitude-longDelta, longitude+longDelta, callback) + } + fun interface Callback { fun onComplete(result: Result) } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java index b5c0653..af4a0d2 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,659 +1,691 @@ /* BusTO - Fragments components 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.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.widget.*; import androidx.annotation.Nullable; import androidx.annotation.NonNull; import androidx.core.widget.NestedScrollView; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Collections; import java.util.List; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.AdapterClickListener; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.data.UserDB; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; import it.reyboz.bustorino.util.LinesNameSorter; import it.reyboz.bustorino.util.ViewUtils; +import static it.reyboz.bustorino.fragments.ScreenBaseFragment.setOption; + public class ArrivalsFragment extends ResultBaseFragment implements LoaderManager.LoaderCallbacks { + private static final String OPTION_SHOW_LEGEND = "show_legend"; private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment"; private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; static final String STOP_TITLE = "messageExtra"; private final static String SOURCES_TEXT="sources_textview_message"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; private boolean fetchersChangeRequestPending = false; private boolean stopIsInFavorites = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; protected TextView messageTextView; protected RecyclerView arrivalsRecyclerView; private PalinaAdapter mListAdapter = null; + private TextView howDoesItWorkTextView; + private Button hideHintButton; + //private NestedScrollView theScrollView; protected RecyclerView noArrivalsRecyclerView; private RouteOnlyLineAdapter noArrivalsAdapter; private TextView noArrivalsTitleView; private GridLayoutManager layoutManager; //private View canaryEndView; private List fetchers = null; //new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; private final AdapterClickListener mRouteClickListener = route -> { String routeName; routeName = FiveTNormalizer.routeInternalToDisplay(route.getNameForDisplay()); if (routeName == null) { routeName = route.getNameForDisplay(); } if(getContext()==null) Log.e(DEBUG_TAG, "Touched on a route but Context is null"); else if (route.destinazione == null || route.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT).show(); } }; public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); } public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null){ args.putString(KEY_STOP_NAME,stopName); } fragment.setArguments(args); return fragment; } public static String getFragmentTag(Palina p) { return "palina_"+p.ID; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID; //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = root.findViewById(R.id.messageTextView); addToFavorites = root.findViewById(R.id.addToFavorites); + // "How does it work part" + howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); + hideHintButton = root.findViewById(R.id.hideHintButton); + hideHintButton.setOnClickListener(this::onHideHint); + //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView); final LinearLayoutManager manager = new LinearLayoutManager(getContext()); arrivalsRecyclerView.setLayoutManager(manager); final DividerItemDecoration mDividerItemDecoration = new DividerItemDecoration(arrivalsRecyclerView.getContext(), manager.getOrientation()); arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration); timesSourceTextView = root.findViewById(R.id.timesSourceTextView); timesSourceTextView.setOnLongClickListener(view -> { if(!fetchersChangeRequestPending){ rotateFetchers(); //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing); mListener.requestArrivalsForStopID(stopID); fetchersChangeRequestPending = true; return true; } return false; }); timesSourceTextView.setOnClickListener(view -> { Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT) .show(); }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites toggleLastStopToFavorites(); }); String displayName = getArguments().getString(STOP_TITLE); if(displayName!=null) setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView); layoutManager = new GridLayoutManager(getContext(),60); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return 12; } }); noArrivalsRecyclerView.setLayoutManager(layoutManager); noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView); //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times if (lastUpdatedPalina!=null) showArrivalsSources(lastUpdatedPalina); return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated+", lastUpdatedPalina is: "+lastUpdatedPalina); /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ /*if(lastUpdatedPalina!=null){ updateFragmentData(null); showArrivalsSources(lastUpdatedPalina); }*/ if (mListAdapter!=null) resetListAdapter(mListAdapter); if(noArrivalsAdapter!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); } if(stopID!=null){ if(!justCreated){ fetchers = utils.getDefaultArrivalsFetchers(getContext()); adjustFetchersToSource(); if (reloadOnResume) mListener.requestArrivalsForStopID(stopID); } else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { Log.d(DEBUG_TAG, "Restarting loader for stop"); loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } + if (ScreenBaseFragment.getOption(requireContext(),OPTION_SHOW_LEGEND, true)) { + showHints(); + } + } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach = false; } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders()); loaderManager.destroyLoader(loaderFavId); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context); } @Nullable public String getStopID() { return stopID; } public boolean reloadsOnResume() { return reloadOnResume; } public void setReloadOnResume(boolean reloadOnResume) { this.reloadOnResume = reloadOnResume; } + // HINT "HOW TO USE" + 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); + } + + public void onHideHint(View v) { + hideHints(); + setOption(requireContext(),OPTION_SHOW_LEGEND, false); + } /** * Give the fetchers * @return the list of the fetchers */ public ArrayList getCurrentFetchers(){ return new ArrayList<>(this.fetchers); } public ArrivalsFetcher[] getCurrentFetchersAsArray(){ ArrivalsFetcher[] arr = new ArrivalsFetcher[fetchers.size()]; fetchers.toArray(arr); return arr; } private void rotateFetchers(){ Log.d(DEBUG_TAG, "Rotating fetchers, before: "+fetchers); Collections.rotate(fetchers, -1); Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: "+fetchers); } /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ if (p!=null) lastUpdatedPalina = p; if (!isAdded()){ //defer update at next show if (p==null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null"); else needUpdateOnAttach = true; } else { final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina, mRouteClickListener, true); showArrivalsSources(lastUpdatedPalina); resetListAdapter(adapter); final ArrayList routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages(); Collections.sort(routesWithNoPassages, new LinesNameSorter()); noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages); if(noArrivalsRecyclerView!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); //hide the views if there are no empty routes if(routesWithNoPassages.isEmpty()){ noArrivalsRecyclerView.setVisibility(View.GONE); noArrivalsTitleView.setVisibility(View.GONE); } else { noArrivalsRecyclerView.setVisibility(View.VISIBLE); noArrivalsTitleView.setVisibility(View.VISIBLE); } } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); if (source == null){ Log.e(DEBUG_TAG, "NULL SOURCE"); return; } String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; case MatoAPI: source_txt = getString(R.string.source_mato); break; case UNDETERMINED: //Don't show the view source_txt = getString(R.string.undetermined_source); break; default: throw new IllegalStateException("Unexpected value: " + source); } // final boolean updatedFetchers = adjustFetchersToSource(source); if(!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work"); final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setText(base_message); timesSourceTextView.setVisibility(View.VISIBLE); if (p.getTotalNumberOfPassages() > 0) { timesSourceTextView.setVisibility(View.VISIBLE); } else { timesSourceTextView.setVisibility(View.INVISIBLE); } fetchersChangeRequestPending = false; } protected boolean adjustFetchersToSource(Passaggio.Source source){ if (source == null) return false; int count = 0; if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 200){ //we need to update the fetcher that is requested rotateFetchers(); count++; } return count < 200; } protected boolean adjustFetchersToSource(){ if (lastUpdatedPalina == null) return false; final Passaggio.Source source = lastUpdatedPalina.getPassaggiSourceIfAny(); return adjustFetchersToSource(source); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && stopName.length() > 0) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ // IT'S IN FAVORITES data.moveToFirst(); final String probableName = data.getString(colUserName); stopIsInFavorites = true; stopName = probableName; //update the message in the textview updateMessage(); } else { stopIsInFavorites =false; } updateStarIcon(); if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); int index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ); if (index == -1){ Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now..."); } stopName = data.getString(index); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } protected void resetListAdapter(PalinaAdapter adapter) { mListAdapter = adapter; if (arrivalsRecyclerView != null) { arrivalsRecyclerView.setAdapter(adapter); arrivalsRecyclerView.setVisibility(View.VISIBLE); } } /** * Set the message textView * @param message the whole message to write in the textView */ public void setTextViewMessage(String message) { messageTextView.setText(message); messageTextView.setVisibility(View.VISIBLE); } public void toggleLastStopToFavorites() { Stop stop = lastUpdatedPalina; if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE, v->updateStarIconFromLastBusStop(v)).execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true); } } /** * Update the star "Add to favorite" icon */ public void updateStarIconFromLastBusStop(Boolean toggleDone) { if (stopIsInFavorites) stopIsInFavorites = !toggleDone; else stopIsInFavorites = toggleDone; updateStarIcon(); // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ public void updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } } @Override public void onDestroyView() { arrivalsRecyclerView = null; if(getArguments()!=null) { getArguments().putString(SOURCES_TEXT, timesSourceTextView.getText().toString()); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); } super.onDestroyView(); } public boolean isFragmentForTheSameStop(Palina p) { if (getTag() != null) return getTag().equals(getFragmentTag(p)); else return false; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java index 934224b..1af84bf 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,889 +1,866 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.net.Uri; 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.coordinatorlayout.widget.CoordinatorLayout; 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.FrameLayout; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.List; 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.AsyncArrivalsSearcher; import it.reyboz.bustorino.middleware.AsyncStopsSearcher; import it.reyboz.bustorino.middleware.BarcodeScanContract; import it.reyboz.bustorino.middleware.BarcodeScanOptions; import it.reyboz.bustorino.middleware.BarcodeScanUtils; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ public class MainScreenFragment extends ScreenBaseFragment 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 static final String PENDING_STOP_SEARCH="PendingStopSearch"; public final static String FRAGMENT_TAG = "MainScreenFragment"; 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 FrameLayout resultFrameLayout; private boolean setupOnStart = true; private boolean suppressArrivalsReload = false; private boolean instanceStateSaved = 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; //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; private final Runnable refreshStop = new Runnable() { public void run() { if(getContext() == null) return; List fetcherList = utils.getDefaultArrivalsFetchers(getContext()); ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()]; arrivalsFetchers = fetcherList.toArray(arrivalsFetchers); 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 Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment"); // AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute(); } else{ String stopName = fragment.getStopID(); new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName); } } else //we create a new fragment, which is WRONG new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(); } }; // private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(), result -> { if(result!=null && result.getContents()!=null) { //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show(); Uri uri; try { uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow. } catch (NullPointerException e) { if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); return; } String busStopID = getBusStopIDFromUri(uri); busStopSearchByIDEditText.setText(busStopID); requestArrivalsForStopID(busStopID); } else { //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); if (getContext()!=null) Toast.makeText(getContext().getApplicationContext(), R.string.no_qrcode, Toast.LENGTH_SHORT).show(); } }); /// LOCATION STUFF /// boolean pendingNearbyStopsRequest = false; boolean locationPermissionGranted, locationPermissionAsked = false; AppLocationManager locationManager; 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) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; boolean resCoarse = result.get(Manifest.permission.ACCESS_COARSE_LOCATION); boolean resFine = result.get(Manifest.permission.ACCESS_FINE_LOCATION); Log.d(DEBUG_TAG, "Permissions for location are: "+result); 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); } // show nearby fragment //showNearbyStopsFragment(); Log.d(DEBUG_TAG, "We have location permission"); if(pendingNearbyStopsRequest){ showNearbyFragmentIfNeeded(cr); pendingNearbyStopsRequest = false; } } if(pendingNearbyStopsRequest) pendingNearbyStopsRequest=false; } }); 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() && checkLocationPermission()){ //request Stops //pendingNearbyStopsRequest = false; if (getContext()!= null && !isNearbyFragmentShown()) //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(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){ // we should have the location permission if(!checkLocationPermission()) Log.e(DEBUG_TAG, "Asking to show nearbystopfragment when " + "we have no location permission"); pendingNearbyStopsRequest = true; //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); showNearbyFragmentIfNeeded(cr); } } @Override public void onLocationDisabled() { } }; //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { return new MainScreenFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing Log.d(DEBUG_TAG, "ARGS ARE NOT NULL: "+getArguments()); if (getArguments().getString(PENDING_STOP_SEARCH)!=null) pendingStopID = getArguments().getString(PENDING_STOP_SEARCH); } } @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); /// UI ELEMENTS // 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); resultFrameLayout = root.findViewById(R.id.resultFrame); 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); coordLayout = root.findViewById(R.id.coord_layout); 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(requireContext()); 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, "Saved instance state is: "+savedInstanceState); //Restore instance state /*if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnStart = false; } } */ if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } instanceStateSaved = false; } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(DEBUG_TAG, "Saving instance state"); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); if (fragmentHelper!=null) fragmentHelper.setBlockAllActivities(true); instanceStateSaved = 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(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+ setupOnStart); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onStart() { super.onStart(); Log.d(DEBUG_TAG, "onStart called, setupOnStart: "+setupOnStart); if (setupOnStart) { if (pendingStopID==null){ //We want the nearby bus stops! //mainHandler.post(new NearbyStopsRequester(getContext(), cr)); Log.d(DEBUG_TAG, "Showing nearby stops"); if(!checkLocationPermission()){ requestLocationPermission(); pendingNearbyStopsRequest = true; } else { showNearbyFragmentIfNeeded(cr); } } else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnStart = false; } } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart); 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); } //don't request permission } 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); } //deactivate suppressArrivalsReload = false; } if(pendingStopID!=null){ Log.d(DEBUG_TAG, "Pending request for arrivals at stop ID: "+pendingStopID); 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(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { BarcodeScanOptions scanOptions = new BarcodeScanOptions(); Intent intent = scanOptions.createScanIntent(); if(!BarcodeScanUtils.checkTargetPackageExists(getContext(), intent)){ BarcodeScanUtils.showDownloadDialog(null, this); }else { barcodeLauncher.launch(scanOptions); } } - 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(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); 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()< 2){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).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); - } - @Nullable @org.jetbrains.annotations.Nullable @Override public View getBaseViewForSnackBar() { return coordLayout; } @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); } private void actuallyShowNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); // fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ Log.d(DEBUG_TAG, "actually showing Nearby Stops Fragment"); //there is no fragment showing final NearbyStopsFragment fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.FragType.STOPS); FragmentTransaction ft = fragMan.beginTransaction(); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); if (getActivity()!=null && !getActivity().isFinishing() &&!instanceStateSaved) ft.commit(); else Log.e(DEBUG_TAG, "Not showing nearby fragment because we saved instanceState"); } } @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) { //if we are getting results, already, stop waiting for nearbyStops if (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS) { hideKeyboard(); if (pendingNearbyStopsRequest) { 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 showLineOnMap(String routeGtfsId) { //pass to activity mListener.showLineOnMap(routeGtfsId); } @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+ " saved: "+pendingStopID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } ArrivalsFetcher[] fetchers = utils.getDefaultArrivalsFetchers(getContext()).toArray(new ArrivalsFetcher[0]); 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 AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID); } } else { Log.d(DEBUG_TAG, "This is probably the first arrivals search, preparing GUI"); prepareGUIForBusLines(); new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } private boolean checkLocationPermission(){ final Context context = getContext(); if(context==null) return false; final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; return isOldVersion || !noPermission; } private void requestLocationPermission(){ requestPermissionLauncher.launch(LOCATION_PERMISSIONS); } private void showNearbyFragmentIfNeeded(Criteria cr){ if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } if(getContext()==null){ Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null"); return; } AppLocationManager appLocationManager = AppLocationManager.getInstance(getContext()); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); actuallyShowNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } /////////// 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/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java index 19a44bb..8883a2e 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/NearbyStopsFragment.java @@ -1,649 +1,624 @@ /* BusTO - Fragments components 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 android.content.SharedPreferences; import android.database.Cursor; import android.location.Location; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.appcompat.widget.AppCompatButton; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.work.WorkInfo; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.*; import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.adapters.ArrivalsStopAdapter; import it.reyboz.bustorino.backend.*; -import it.reyboz.bustorino.backend.FiveTAPIFetcher.QueryType; import it.reyboz.bustorino.backend.mato.MapiArrivalRequest; import it.reyboz.bustorino.data.DatabaseUpdate; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.middleware.AppLocationManager; import it.reyboz.bustorino.data.AppDataProvider; -import it.reyboz.bustorino.data.NextGenDB.Contract.*; import it.reyboz.bustorino.adapters.SquareStopAdapter; import it.reyboz.bustorino.middleware.AutoFitGridLayoutManager; import it.reyboz.bustorino.util.LocationCriteria; import it.reyboz.bustorino.util.StopSorterByDistance; +import it.reyboz.bustorino.viewmodels.NearbyStopsViewModel; +import org.jetbrains.annotations.NotNull; import java.util.*; -public class NearbyStopsFragment extends Fragment implements LoaderManager.LoaderCallbacks { +public class NearbyStopsFragment extends Fragment { public enum FragType{ STOPS(1), ARRIVALS(2); private final int num; FragType(int num){ this.num = num; } public static FragType fromNum(int i){ switch (i){ case 1: return STOPS; case 2: return ARRIVALS; default: throw new IllegalArgumentException("type not recognized"); } } } private FragmentListenerMain mListener; private FragmentLocationListener fragmentLocationListener; private final static String DEBUG_TAG = "NearbyStopsFragment"; private final static String FRAGMENT_TYPE_KEY = "FragmentType"; //public final static int TYPE_STOPS = 19, TYPE_ARRIVALS = 20; private FragType fragment_type = FragType.STOPS; public final static String FRAGMENT_TAG="NearbyStopsFrag"; //data Bundle private final String BUNDLE_LOCATION = "location"; private final int LOADER_ID = 0; private RecyclerView gridRecyclerView; private SquareStopAdapter dataAdapter; private AutoFitGridLayoutManager gridLayoutManager; - private Location lastReceivedLocation = null; + private GPSPoint lastPosition = null; private ProgressBar circlingProgressBar,flatProgressBar; - private int distance; + private int distance = 10; protected SharedPreferences globalSharedPref; private SharedPreferences.OnSharedPreferenceChangeListener preferenceChangeListener; private TextView messageTextView,titleTextView; private CommonScrollListener scrollListener; private AppCompatButton switchButton; private boolean firstLocForStops = true,firstLocForArrivals = true; public static final int COLUMN_WIDTH_DP = 250; private Integer MAX_DISTANCE = -3; private int MIN_NUM_STOPS = -1; private int TIME_INTERVAL_REQUESTS = -1; private AppLocationManager locManager; //These are useful for the case of nearby arrivals private ArrivalsManager arrivalsManager = null; private ArrivalsStopAdapter arrivalsStopAdapter = null; private boolean dbUpdateRunning = false; private ArrayList currentNearbyStops = new ArrayList<>(); + //ViewModel + private NearbyStopsViewModel viewModel; + public NearbyStopsFragment() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * @return A new instance of fragment NearbyStopsFragment. */ public static NearbyStopsFragment newInstance(FragType type) { //if(fragmentType != TYPE_STOPS && fragmentType != TYPE_ARRIVALS ) // throw new IllegalArgumentException("WRONG KIND OF FRAGMENT USED"); NearbyStopsFragment fragment = new NearbyStopsFragment(); final Bundle args = new Bundle(1); args.putInt(FRAGMENT_TYPE_KEY,type.num); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { setFragmentType(FragType.fromNum(getArguments().getInt(FRAGMENT_TYPE_KEY))); } locManager = AppLocationManager.getInstance(getContext()); - fragmentLocationListener = new FragmentLocationListener(this); + fragmentLocationListener = new FragmentLocationListener(); if (getContext()!=null) { globalSharedPref = getContext().getSharedPreferences(getString(R.string.mainSharedPreferences), Context.MODE_PRIVATE); globalSharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment if (getContext() == null) throw new RuntimeException(); View root = inflater.inflate(R.layout.fragment_nearby_stops, container, false); gridRecyclerView = root.findViewById(R.id.stopGridRecyclerView); gridLayoutManager = new AutoFitGridLayoutManager(getContext().getApplicationContext(), Float.valueOf(utils.convertDipToPixels(getContext(),COLUMN_WIDTH_DP)).intValue()); gridRecyclerView.setLayoutManager(gridLayoutManager); gridRecyclerView.setHasFixedSize(false); circlingProgressBar = root.findViewById(R.id.loadingBar); flatProgressBar = root.findViewById(R.id.horizontalProgressBar); messageTextView = root.findViewById(R.id.messageTextView); titleTextView = root.findViewById(R.id.titleTextView); switchButton = root.findViewById(R.id.switchButton); scrollListener = new CommonScrollListener(mListener,false); switchButton.setOnClickListener(v -> switchFragmentType()); Log.d(DEBUG_TAG, "onCreateView"); DatabaseUpdate.watchUpdateWorkStatus(getContext(), this, new Observer>() { @Override public void onChanged(List workInfos) { if(workInfos.isEmpty()) return; WorkInfo wi = workInfos.get(0); if (wi.getState() == WorkInfo.State.RUNNING && locManager.isRequesterRegistered(fragmentLocationListener)) { locManager.removeLocationRequestFor(fragmentLocationListener); dbUpdateRunning = true; - } else if(!locManager.isRequesterRegistered(fragmentLocationListener)){ - locManager.addLocationRequestFor(fragmentLocationListener); + } else{ + //start the request + if(!locManager.isRequesterRegistered(fragmentLocationListener)) + locManager.addLocationRequestFor(fragmentLocationListener); dbUpdateRunning = false; } } }); + + //observe the livedata + viewModel.getStopsAtDistance().observe(getViewLifecycleOwner(), stops -> { + if (!dbUpdateRunning && (stops.size() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { + distance = distance + 40; + viewModel.requestStopsAtDistance(distance, true); + //Log.d(DEBUG_TAG, "Doubling distance now!"); + return; + } + if(!stops.isEmpty()) { + currentNearbyStops =stops; + showStopsInViews(currentNearbyStops, lastPosition); + } + }); return root; } + /** * Use this method to set the fragment type * @param type the type, TYPE_ARRIVALS or TYPE_STOPS */ private void setFragmentType(FragType type){ this.fragment_type = type; switch(type){ case ARRIVALS: TIME_INTERVAL_REQUESTS = 5*1000; break; case STOPS: TIME_INTERVAL_REQUESTS = 1000; } } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - /// TODO: RISOLVERE PROBLEMA: il context qui e' l'Activity non il Fragment if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context + " must implement OnFragmentInteractionListener"); } Log.d(DEBUG_TAG, "OnAttach called"); + viewModel = new ViewModelProvider(this).get(NearbyStopsViewModel.class); } @Override public void onPause() { super.onPause(); gridRecyclerView.setAdapter(null); locManager.removeLocationRequestFor(fragmentLocationListener); Log.d(DEBUG_TAG,"On paused called"); } @Override public void onResume() { super.onResume(); try{ if(!dbUpdateRunning && !locManager.isRequesterRegistered(fragmentLocationListener)) locManager.addLocationRequestFor(fragmentLocationListener); } catch (SecurityException ex){ //ignored //try another location provider } switch(fragment_type){ case STOPS: if(dataAdapter!=null){ gridRecyclerView.setAdapter(dataAdapter); circlingProgressBar.setVisibility(View.GONE); } break; case ARRIVALS: if(arrivalsStopAdapter!=null){ gridRecyclerView.setAdapter(arrivalsStopAdapter); circlingProgressBar.setVisibility(View.GONE); } } mListener.enableRefreshLayout(false); Log.d(DEBUG_TAG,"OnResume called"); if(getContext()==null){ Log.e(DEBUG_TAG, "NULL CONTEXT, everything is going to crash now"); MIN_NUM_STOPS = 5; MAX_DISTANCE = 600; return; } //Re-read preferences SharedPreferences shpr = PreferenceManager.getDefaultSharedPreferences(getContext().getApplicationContext()); //For some reason, they are all saved as strings MAX_DISTANCE = shpr.getInt(getString(R.string.pref_key_radius_recents),600); boolean isMinStopInt = true; try{ MIN_NUM_STOPS = shpr.getInt(getString(R.string.pref_key_num_recents), 5); } catch (ClassCastException ex){ isMinStopInt = false; } if(!isMinStopInt) try { MIN_NUM_STOPS = Integer.parseInt(shpr.getString(getString(R.string.pref_key_num_recents), "5")); } catch (NumberFormatException ex){ MIN_NUM_STOPS = 5; } if(BuildConfig.DEBUG) - Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ - ", Min number of stops: "+MIN_NUM_STOPS); + Log.d(DEBUG_TAG, "Max distance for stops: "+MAX_DISTANCE+ ", Min number of stops: "+MIN_NUM_STOPS); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); gridRecyclerView.setVisibility(View.INVISIBLE); gridRecyclerView.addOnScrollListener(scrollListener); } @Override public void onDetach() { super.onDetach(); mListener = null; if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); } - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - //BUILD URI - if (args!=null) - lastReceivedLocation = args.getParcelable(BUNDLE_LOCATION); - Uri.Builder builder = new Uri.Builder(); - builder.scheme("content").authority(AppDataProvider.AUTHORITY) - .appendPath("stops").appendPath("location") - .appendPath(String.valueOf(lastReceivedLocation.getLatitude())) - .appendPath(String.valueOf(lastReceivedLocation.getLongitude())) - .appendPath(String.valueOf(distance)); //distance - CursorLoader cl = new CursorLoader(getContext(),builder.build(),NextGenDB.QUERY_COLUMN_stops_all,null,null,null); - cl.setUpdateThrottle(2000); - return cl; - } - - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - if (0 > MAX_DISTANCE) throw new AssertionError(); - //Cursor might be null - if (cursor == null) { - Log.e(DEBUG_TAG, "Null cursor, something really wrong happened"); - return; - } - Log.d(DEBUG_TAG, "Num stops found: " + cursor.getCount() + ", Current distance: " + distance); - - if (!dbUpdateRunning && (cursor.getCount() < MIN_NUM_STOPS && distance <= MAX_DISTANCE)) { - distance = distance * 2; - Bundle d = new Bundle(); - d.putParcelable(BUNDLE_LOCATION, lastReceivedLocation); - getLoaderManager().restartLoader(LOADER_ID, d, this); - //Log.d(DEBUG_TAG, "Doubling distance now!"); - return; - } - Log.d("LoadFromCursor", "Number of nearby stops: " + cursor.getCount()); - //////// - if(cursor.getCount()>0) - currentNearbyStops = NextGenDB.getStopsFromCursorAllFields(cursor); - - showCurrentStops(); - } - /** * Display the stops, or run new set of requests for arrivals */ - private void showCurrentStops(){ - if (currentNearbyStops.isEmpty()) { + private void showStopsInViews(ArrayList stops, GPSPoint location){ + if (stops.isEmpty()) { setNoStopsLayout(); return; } double minDistance = Double.POSITIVE_INFINITY; - for(Stop s: currentNearbyStops){ - minDistance = Math.min(minDistance, s.getDistanceFromLocation(lastReceivedLocation)); + for(Stop s: stops){ + minDistance = Math.min(minDistance, s.getDistanceFromLocation(location.getLatitude(), location.getLongitude())); } //quick trial to hopefully always get the stops in the correct order - Collections.sort(currentNearbyStops,new StopSorterByDistance(lastReceivedLocation)); + Collections.sort(stops,new StopSorterByDistance(location)); switch (fragment_type){ case STOPS: - showStopsInRecycler(currentNearbyStops); + showStopsInRecycler(stops); break; case ARRIVALS: - arrivalsManager = new ArrivalsManager(currentNearbyStops); + arrivalsManager = new ArrivalsManager(stops); flatProgressBar.setVisibility(View.VISIBLE); flatProgressBar.setProgress(0); flatProgressBar.setIndeterminate(false); //for the moment, be satisfied with only one location //AppLocationManager.getInstance(getContext()).removeLocationRequestFor(fragmentLocationListener); break; default: } } - @Override - public void onLoaderReset(@NonNull Loader loader) { - } - /** * To enable targeting from the Button */ public void switchFragmentType(View v){ switchFragmentType(); } /** * Call when you need to switch the type of fragment */ private void switchFragmentType(){ if(fragment_type==FragType.ARRIVALS){ setFragmentType(FragType.STOPS); switchButton.setText(getString(R.string.show_arrivals)); titleTextView.setText(getString(R.string.nearby_stops_message)); if(arrivalsManager!=null) arrivalsManager.cancelAllRequests(); if(dataAdapter!=null) gridRecyclerView.setAdapter(dataAdapter); } else if (fragment_type==FragType.STOPS){ setFragmentType(FragType.ARRIVALS); titleTextView.setText(getString(R.string.nearby_arrivals_message)); switchButton.setText(getString(R.string.show_stops)); if(arrivalsStopAdapter!=null) gridRecyclerView.setAdapter(arrivalsStopAdapter); } fragmentLocationListener.lastUpdateTime = -1; //locManager.removeLocationRequestFor(fragmentLocationListener); //locManager.addLocationRequestFor(fragmentLocationListener); - showCurrentStops(); + showStopsInViews(currentNearbyStops, lastPosition); } //useful methods /////// GUI METHODS //////// private void showStopsInRecycler(List stops){ if(firstLocForStops) { - dataAdapter = new SquareStopAdapter(stops, mListener, lastReceivedLocation); + dataAdapter = new SquareStopAdapter(stops, mListener, lastPosition); gridRecyclerView.setAdapter(dataAdapter); firstLocForStops = false; }else { dataAdapter.setStops(stops); - dataAdapter.setUserPosition(lastReceivedLocation); + dataAdapter.setUserPosition(lastPosition); } dataAdapter.notifyDataSetChanged(); //showRecyclerHidingLoadMessage(); if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_STOPS); } private void showArrivalsInRecycler(List palinas){ - Collections.sort(palinas,new StopSorterByDistance(lastReceivedLocation)); + Collections.sort(palinas,new StopSorterByDistance(lastPosition)); final ArrayList> routesPairList = new ArrayList<>(10); //int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: palinas){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything if (r.passaggi != null && !r.passaggi.isEmpty()) routesPairList.add(new Pair<>(p,r)); } } if (getContext()==null){ Log.e(DEBUG_TAG, "Trying to show arrivals in Recycler but we're not attached"); return; } if(firstLocForArrivals){ - arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastReceivedLocation); + arrivalsStopAdapter = new ArrivalsStopAdapter(routesPairList,mListener,getContext(),lastPosition); gridRecyclerView.setAdapter(arrivalsStopAdapter); firstLocForArrivals = false; } else { - arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastReceivedLocation); + arrivalsStopAdapter.setRoutesPairListAndPosition(routesPairList,lastPosition); } //arrivalsStopAdapter.notifyDataSetChanged(); showRecyclerHidingLoadMessage(); if(mListener!=null) mListener.readyGUIfor(FragmentKind.NEARBY_ARRIVALS); } private void setNoStopsLayout(){ messageTextView.setVisibility(View.VISIBLE); messageTextView.setText(R.string.no_stops_nearby); circlingProgressBar.setVisibility(View.GONE); } /** * Does exactly what is says on the tin */ private void showRecyclerHidingLoadMessage(){ if (gridRecyclerView.getVisibility() != View.VISIBLE) { circlingProgressBar.setVisibility(View.GONE); gridRecyclerView.setVisibility(View.VISIBLE); } messageTextView.setVisibility(View.GONE); } class ArrivalsManager implements Response.Listener, Response.ErrorListener{ final HashMap palinasDone = new HashMap<>(); //final Map> routesToAdd = new HashMap<>(); final static String REQUEST_TAG = "NearbyArrivals"; final NetworkVolleyManager volleyManager; int activeRequestCount = 0,reqErrorCount = 0, reqSuccessCount=0; ArrivalsManager(List stops){ volleyManager = NetworkVolleyManager.getInstance(getContext()); int MAX_ARRIVAL_STOPS = 35; Date currentDate = new Date(); int timeRange = 3600; int departures = 10; int numreq = 0; for(Stop s: stops.subList(0,Math.min(stops.size(), MAX_ARRIVAL_STOPS))){ final MapiArrivalRequest req = new MapiArrivalRequest(s.ID, currentDate, timeRange, departures, this, this); req.setTag(REQUEST_TAG); volleyManager.addToRequestQueue(req); activeRequestCount++; numreq++; } flatProgressBar.setMax(numreq); } @Override public void onErrorResponse(VolleyError error) { if(error instanceof ParseError){ //TODO Log.w(DEBUG_TAG,"Parsing error for stop request"); } else if (error instanceof NetworkError){ String s; if(error.networkResponse!=null) s = new String(error.networkResponse.data); else s=""; Log.w(DEBUG_TAG,"Network error: "+s); }else { Log.w(DEBUG_TAG,"Volley Error: "+error.getMessage()); } if(error.networkResponse!=null){ Log.w(DEBUG_TAG, "Error status code: "+error.networkResponse.statusCode); } //counters activeRequestCount--; reqErrorCount++; flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); } @Override public void onResponse(Palina result) { //counter for requests activeRequestCount--; reqSuccessCount++; //final Palina palinaInMap = palinasDone.get(result.ID); //palina cannot be null here //sorry for the brutal crash when it happens //if(palinaInMap == null) throw new IllegalStateException("Cannot get the palina from the map"); //add the palina to the successful one //TODO: Avoid redoing everything every time a new Result arrives palinasDone.put(result.ID, result); final ArrayList outList = new ArrayList<>(); for(Palina p: palinasDone.values()){ final List routes = p.queryAllRoutes(); if(routes!=null && routes.size()>0) outList.add(p); } showArrivalsInRecycler(outList); flatProgressBar.setProgress(reqErrorCount+reqSuccessCount); if(activeRequestCount==0) { flatProgressBar.setIndeterminate(true); flatProgressBar.setVisibility(View.GONE); } } void cancelAllRequests(){ volleyManager.getRequestQueue().cancelAll(REQUEST_TAG); flatProgressBar.setVisibility(View.GONE); } } /** * Local locationListener, to use for the GPS */ class FragmentLocationListener implements AppLocationManager.LocationRequester{ - LoaderManager.LoaderCallbacks callbacks; private int oldLocStatus = -2; private LocationCriteria cr; private long lastUpdateTime = -1; - public FragmentLocationListener(LoaderManager.LoaderCallbacks callbacks) { - this.callbacks = callbacks; - } @Override public void onLocationChanged(Location location) { //set adapter - float accuracy = location.getAccuracy(); - if(accuracy<100 && !dbUpdateRunning) { - distance = 20; - final Bundle msgBundle = new Bundle(); - msgBundle.putParcelable(BUNDLE_LOCATION,location); - getLoaderManager().restartLoader(LOADER_ID,msgBundle,callbacks); + + if(location==null){ + Log.e(DEBUG_TAG, "Location is null, cannot request stops"); + return; + } else if(viewModel==null){ + return; + } + if(location.getAccuracy()<100 && !dbUpdateRunning) { + if(viewModel.getDistanceMtLiveData().getValue()==null){ + //never run request + distance = 40; + } + lastPosition = new GPSPoint(location.getLatitude(), location.getLongitude()); + viewModel.requestStopsAtDistance(location.getLatitude(), location.getLongitude(), distance, true); } lastUpdateTime = System.currentTimeMillis(); - Log.d("BusTO:NearPositListen","can start loader "+ !dbUpdateRunning); + Log.d("BusTO:NearPositListen","can start request for stops: "+ !dbUpdateRunning); } @Override public void onLocationStatusChanged(int status) { switch(status){ case AppLocationManager.LOCATION_GPS_AVAILABLE: messageTextView.setVisibility(View.GONE); break; case AppLocationManager.LOCATION_UNAVAILABLE: messageTextView.setText(R.string.enableGpsText); messageTextView.setVisibility(View.VISIBLE); break; default: Log.e(DEBUG_TAG,"Location status not recognized"); } } @Override - public LocationCriteria getLocationCriteria() { + public @NotNull LocationCriteria getLocationCriteria() { - return new LocationCriteria(120,TIME_INTERVAL_REQUESTS); + return new LocationCriteria(200,TIME_INTERVAL_REQUESTS); } @Override public long getLastUpdateTimeMillis() { return lastUpdateTime; } void resetUpdateTime(){ lastUpdateTime = -1; } @Override public void onLocationProviderAvailable() { } @Override public void onLocationDisabled() { } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index 81bb88d..25971d4 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -1,55 +1,65 @@ package it.reyboz.bustorino.fragments; import android.content.Context; import android.content.SharedPreferences; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.BuildConfig; import static android.content.Context.MODE_PRIVATE; public abstract class ScreenBaseFragment extends Fragment { - protected final String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs"; + protected final static String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs"; protected void setOption(String optionName, boolean value) { Context mContext = getContext(); SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { Context mContext = getContext(); - SharedPreferences preferences = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE); - return preferences.getBoolean(optionName, optDefault); + assert mContext != null; + return getOption(mContext, optionName, optDefault); } protected void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Toast.makeText(getContext(), messageID, length).show(); } public void hideKeyboard() { if (getActivity()==null) return; View view = getActivity().getCurrentFocus(); if (view != null) { ((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } /** * Find the view on which the snackbar should be shown * @return */ @Nullable public abstract View getBaseViewForSnackBar(); + + public static boolean getOption(Context context, String optionName, boolean optDefault){ + SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE); + return preferences.getBoolean(optionName, optDefault); + } + public static void setOption(Context context,String optionName, boolean value) { + SharedPreferences.Editor editor = context.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); + editor.putBoolean(optionName, value); + editor.apply(); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java index 055d9c5..abb9b0f 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java +++ b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java @@ -1,71 +1,76 @@ /* BusTO (util) Copyright (C) 2019 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.util; import android.location.Location; import androidx.core.util.Pair; import android.util.Log; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.utils; +import org.osmdroid.api.IGeoPoint; import java.util.Collections; import java.util.Comparator; import java.util.List; public class RoutePositionSorter implements Comparator> { - private final Location loc; + private final double latPos, longPos; private final double minutialmetro = 6.0/100; //v = 5km/h private final double distancemultiplier = 2./3; - public RoutePositionSorter(Location loc) { - this.loc = loc; + public RoutePositionSorter(double latitude, double longitude){ + latPos = latitude; + longPos = longitude; + } + public RoutePositionSorter(IGeoPoint position){ + this(position.getLatitude(), position.getLongitude()); } @Override public int compare(Pair pair1, Pair pair2) throws NullPointerException{ int delta = 0; final Stop stop1 = pair1.first, stop2 = pair2.first; - double dist1 = utils.measuredistanceBetween(loc.getLatitude(),loc.getLongitude(), + double dist1 = utils.measuredistanceBetween(latPos,longPos, stop1.getLatitude(),stop1.getLongitude()); - double dist2 = utils.measuredistanceBetween(loc.getLatitude(),loc.getLongitude(), + double dist2 = utils.measuredistanceBetween(latPos,longPos, stop2.getLatitude(),stop2.getLongitude()); final List passaggi1 = pair1.second.passaggi, passaggi2 = pair2.second.passaggi; if(passaggi1.size()<=0 || passaggi2.size()<=0){ Log.e("ArrivalsStopAdapter","Cannot compare: No arrivals in one of the stops"); } else { Collections.sort(passaggi1); Collections.sort(passaggi2); int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh; if(deltaOre>12) deltaOre -= 24; else if (deltaOre<-12) deltaOre += 24; delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm; } delta += (int)((dist1 -dist2)*minutialmetro*distancemultiplier); return delta; } @Override public boolean equals(Object obj) { return obj instanceof RoutePositionSorter; } } diff --git a/app/src/main/java/it/reyboz/bustorino/util/StopSorterByDistance.java b/app/src/main/java/it/reyboz/bustorino/util/StopSorterByDistance.java index 03f80b4..89042ad 100644 --- a/app/src/main/java/it/reyboz/bustorino/util/StopSorterByDistance.java +++ b/app/src/main/java/it/reyboz/bustorino/util/StopSorterByDistance.java @@ -1,36 +1,38 @@ /* BusTO (util) Copyright (C) 2019 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.util; import android.location.Location; +import it.reyboz.bustorino.backend.GPSPoint; import it.reyboz.bustorino.backend.Stop; import java.util.Comparator; public class StopSorterByDistance implements Comparator { - private final Location locToCompare; + private final double latitude; + private final double longitude; - public StopSorterByDistance(Location locToCompare) { - this.locToCompare = locToCompare; + public StopSorterByDistance(GPSPoint geoPoint) { + latitude = geoPoint.getLatitude(); + longitude = geoPoint.getLongitude(); } - @Override public int compare(Stop o1, Stop o2) { - return (int) (o1.getDistanceFromLocation(locToCompare)-o2.getDistanceFromLocation(locToCompare)); + return (int) (o1.getDistanceFromLocation(latitude, longitude)-o2.getDistanceFromLocation(latitude, longitude)); } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt new file mode 100644 index 0000000..0da572d --- /dev/null +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/NearbyStopsViewModel.kt @@ -0,0 +1,75 @@ +package it.reyboz.bustorino.viewmodels + +import android.app.Application +import android.location.Location +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import it.reyboz.bustorino.backend.GPSPoint +import it.reyboz.bustorino.backend.Stop +import it.reyboz.bustorino.data.OldDataRepository +import java.util.ArrayList +import java.util.concurrent.Executors + +class NearbyStopsViewModel(application: Application): AndroidViewModel(application) { + + private val executor = Executors.newFixedThreadPool(2) + private val oldRepo = OldDataRepository(executor, application) + + + val locationLiveData = MutableLiveData() + val distanceMtLiveData = MutableLiveData(40) + + + val stopsAtDistance = MutableLiveData>() + + private val callback = + OldDataRepository.Callback> { res -> + if(res.isSuccess){ + stopsAtDistance.postValue(res.result) + Log.d(DEBUG_TAG, "Setting value of stops in bounding box") + } + } + + /** + * Request stop in location [latitude], [longitude], at distance [distanceMeters] + * If [saveValues] is true, store the position and the distance used + */ + fun requestStopsAtDistance(latitude: Double, longitude: Double, distanceMeters: Int, saveValues: Boolean){ + if(saveValues){ + locationLiveData.postValue(GPSPoint(latitude, longitude)) + distanceMtLiveData.postValue(distanceMeters) + } + oldRepo.requestStopsWithinDistance(latitude, longitude, distanceMeters, callback) + } + + /** + * Request stops using the previously saved location + */ + fun requestStopsAtDistance(distanceMeters: Int, saveValue: Boolean){ + if(saveValue){ + distanceMtLiveData.postValue(distanceMeters) + } + oldRepo.requestStopsWithinDistance( + locationLiveData.value!!.latitude, + locationLiveData.value!!.longitude, distanceMeters, callback) + } + + + fun setLocation(location: Location){ + locationLiveData.postValue(GPSPoint(location.latitude, location.longitude)) + } + fun setLocation(location: GPSPoint){ + locationLiveData.postValue(location) + } + fun setLastDistance(distanceMeters: Int){ + distanceMtLiveData.postValue(distanceMeters) + } + + + + + companion object{ + private const val DEBUG_TAG = "BusTO-NearbyStopVwModel" + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/top_bottom_border_radius.xml b/app/src/main/res/drawable/top_bottom_border_radius.xml new file mode 100644 index 0000000..d8c48a1 --- /dev/null +++ b/app/src/main/res/drawable/top_bottom_border_radius.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/top_bottom_border_radius_with_border.xml b/app/src/main/res/drawable/top_bottom_border_radius_with_border.xml new file mode 100644 index 0000000..f65bfa1 --- /dev/null +++ b/app/src/main/res/drawable/top_bottom_border_radius_with_border.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_arrivals.xml b/app/src/main/res/layout/fragment_arrivals.xml index d94d80d..993442c 100644 --- a/app/src/main/res/layout/fragment_arrivals.xml +++ b/app/src/main/res/layout/fragment_arrivals.xml @@ -1,147 +1,184 @@ + android:paddingTop="8dp" + android:animateLayoutChanges="true"> + + +