Page MenuHomeGitPull.it

No OneTemporary

Authored By
Unknown
Size
270 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java
index f8aef94..7811de6 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java
@@ -1,417 +1,475 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.backend;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import it.reyboz.bustorino.util.LinesNameSorter;
/**
* Timetable for multiple routes.<br>
* <br>
* Apparently "palina" and a bunch of other terms can't really be translated into English.<br>
* Not in a way that makes sense and keeps the code readable, at least.
*/
-public class Palina extends Stop {
+public class Palina extends Stop implements Parcelable {
private ArrayList<Route> routes = new ArrayList<>();
private boolean routesModified = false;
private Passaggio.Source allSource = null;
public Palina(String stopID) {
super(stopID);
}
public Palina(Stop s){
super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type,
s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), null);
}
public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName,
@Nullable String location,
@Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) {
super(ID, name, userName, location, null, null, lat, lon, gtfsID);
}
public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List<String> routesThatStopHere) {
super(name, ID, location, type, routesThatStopHere);
}
/**
* Adds a timetable entry to a route.
*
* @param TimeGTT time in GTT format (e.g. "11:22*")
* @param arrayIndex position in the array for this route (returned by addRoute)
*/
public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) {
this.routes.get(arrayIndex).addPassaggio(TimeGTT,src);
routesModified = true;
}
/**
* Count routes with missing directions
* @return number
*/
public int countRoutesWithMissingDirections(){
int i = 0;
for (Route r : routes){
if(r.destinazione==null||r.destinazione.equals(""))
i++;
}
return i;
}
/**
* Adds a route to the timetable.
*
* @param routeID name
* @param type bus, underground, railway, ...
* @param destinazione end of line\terminus (underground stations have the same ID for both directions)
* @return array index for this route
*/
public int addRoute(String routeID, String destinazione, Route.Type type) {
return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6)));
}
public int addRoute(Route r){
this.routes.add(r);
routesModified = true;
buildRoutesString();
return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct
}
public void setRoutes(List<Route> routeList){
routes = new ArrayList<>(routeList);
}
@Nullable
@Override
protected String buildRoutesString() {
// no routes => no string
if(routes == null || routes.size() == 0) {
return "";
}
/*final StringBuilder sb = new StringBuilder();
final LinesNameSorter nameSorter = new LinesNameSorter();
Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim()));
int i, lenMinusOne = routes.size() - 1;
for (i = 0; i < lenMinusOne; i++) {
sb.append(routes.get(i).getName().trim()).append(", ");
}
// last one:
sb.append(routes.get(i).getName());
*/
ArrayList<String> names = new ArrayList<>();
for (Route r: routes){
names.add(r.getName());
}
final String routesThatStopHere = buildRoutesStringFromNames(names);
setRoutesThatStopHereString(routesThatStopHere);
return routesThatStopHereToString();
}
/**
* Sort the names of the routes for the string "routes stopping here" and make the string
* @param names of the Routes that pass in the stop
* @return the full string of routes stopping (eg, "10, 13, 42" ecc)
*/
public static String buildRoutesStringFromNames(List<String> names){
final StringBuilder sb = new StringBuilder();
final LinesNameSorter nameSorter = new LinesNameSorter();
Collections.sort(names, nameSorter);
int i, lenMinusOne = names.size() - 1;
for (i = 0; i < lenMinusOne; i++) {
sb.append(names.get(i).trim()).append(", ");
}
//last one
sb.append(names.get(i).trim());
return sb.toString();
}
protected void checkPassaggi(){
Passaggio.Source mSource = null;
for (Route r: routes){
for(Passaggio pass: r.passaggi){
if (mSource == null) {
mSource = pass.source;
} else if (mSource != pass.source){
Log.w("BusTO-CheckPassaggi",
"Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source );
mSource = Passaggio.Source.UNDETERMINED;
break;
}
}
if(mSource == Passaggio.Source.UNDETERMINED)
break;
}
// if the Source is still null, set undetermined
if (mSource == null) mSource = Passaggio.Source.UNDETERMINED;
//finished with the check, setting flags
routesModified = false;
allSource = mSource;
}
@NonNull
public Passaggio.Source getPassaggiSourceIfAny(){
if(allSource==null || routesModified){
checkPassaggi();
}
assert allSource != null;
return allSource;
}
/**
* Gets every route and its timetable.
*
* @return routes and timetables.
*/
public List<Route> queryAllRoutes() {
return this.routes;
}
public void sortRoutes() {
Collections.sort(this.routes);
}
/**
* Add info about the routes already found from another source
* @param additionalRoutes ArrayList of routes to get the info from
* @return the number of routes modified
*/
public int addInfoFromRoutes(List<Route> additionalRoutes){
if(routes == null || routes.size()==0) {
this.routes = new ArrayList<>(additionalRoutes);
buildRoutesString();
return routes.size();
}
int count=0;
final Calendar c = Calendar.getInstance();
final int todaysInt = c.get(Calendar.DAY_OF_WEEK);
for(Route r:routes) {
int j = 0;
boolean correct = false;
Route selected = null;
//TODO: rewrite this as a simple loop
//MADNESS begins here
while (!correct) {
//find the correct route to merge to
// scan routes and find the first which has the same name
while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) {
j++;
}
if (j == additionalRoutes.size()) break; //no match has been found
//should have found the first occurrence of the line
selected = additionalRoutes.get(j);
//move forward
j++;
if (selected.serviceDays != null && selected.serviceDays.length > 0) {
//check if it is in service
for (int d : selected.serviceDays) {
if (d == todaysInt) {
correct = true;
break;
}
}
} else if (r.festivo != null) {
switch (r.festivo) {
case FERIALE:
//Domenica = 1 --> Saturday=7
if (todaysInt <= 7 && todaysInt > 1) correct = true;
break;
case FESTIVO:
if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays
break;
case UNKNOWN:
correct = true;
}
} else {
//case a: there is no info because the line is always active
//case b: there is no info because the information is missing
correct = true;
}
}
if (!correct || selected == null) {
Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName());
continue; //we didn't find any match
}
//found the correct correspondance
//MERGE INFO
if(r.mergeRouteWithAnother(selected)) count++;
}
if (count> 0) buildRoutesString();
return count;
}
// /**
// * Route with terminus (destinazione) and timetables (passaggi), internal implementation.
// *
// * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here.
// */
// private final class RouteInternal {
// public final String name;
// public final String destinazione;
// private boolean updated;
// private List<Passaggio> passaggi;
//
// /**
// * Creates a new route and marks it as "updated", since it's new.
// *
// * @param routeID name
// * @param destinazione end of line\terminus
// */
// public RouteInternal(String routeID, String destinazione) {
// this.name = routeID;
// this.destinazione = destinazione;
// this.passaggi = new LinkedList<>();
// this.updated = true;
// }
//
// /**
// * Adds a time (passaggio) to the timetable for this route
// *
// * @param TimeGTT time in GTT format (e.g. "11:22*")
// */
// public void addPassaggio(String TimeGTT) {
// this.passaggi.add(new Passaggio(TimeGTT));
// }
//
// /**
// * Deletes al times (passaggi) from the timetable.
// */
// public void deletePassaggio() {
// this.passaggi = new LinkedList<>();
// this.updated = true;
// }
//
// /**
// * Sets the "updated" flag to false.
// *
// * @return previous state
// */
// public boolean unupdateFlag() {
// if(this.updated) {
// this.updated = false;
// return true;
// } else {
// return false;
// }
// }
//
// /**
// * Sets the "updated" flag to true.
// *
// * @return previous state
// */
// public boolean updateFlag() {
// if(this.updated) {
// return true;
// } else {
// this.updated = true;
// return false;
// }
// }
//
// /**
// * Exactly what it says on the tin.
// *
// * @return times from the timetable
// */
// public List<Passaggio> getPassaggi() {
// return this.passaggi;
// }
// }
//remove duplicates
public void mergeDuplicateRoutes(int startidx){
//ArrayList<Route> routesCopy = new ArrayList<>(routes);
//for
if(routes.size()<=1|| startidx >= routes.size()) //we have finished
return;
Route routeCheck = routes.get(startidx);
boolean found = false;
for(int i=startidx+1; i<routes.size(); i++){
final Route r = routes.get(i);
if(routeCheck.equals(r)){
//we have found a match, merge
routes.remove(routeCheck);
r.mergeRouteWithAnother(routeCheck);
found=true;
break;
}
}
if (found) mergeDuplicateRoutes(startidx);
else mergeDuplicateRoutes(startidx+1);
}
public int getTotalNumberOfPassages(){
int tot = 0;
if(routes==null)
return tot;
for(Route r: routes){
tot += r.numPassaggi();
}
return tot;
}
/**
* Compute the minimum number of passages per route
* Ignoring empty routes
* @return the minimum, or 0 if there are no passages/routes
*/
public int getMinNumberOfPassages(){
if (routes == null) return 0;
int min = Integer.MAX_VALUE;
if( routes.size() == 0) min = 0;
else for (Route r : routes){
if(r.numPassaggi()>0)
min = Math.min(min,r.numPassaggi());
}
if (min == Integer.MAX_VALUE) return 0;
else return min;
}
public ArrayList<String> getRoutesNamesWithNoPassages(){
ArrayList<String> mList = new ArrayList<>();
if(routes==null || routes.size() == 0){
return mList;
}
for(Route r: routes){
if(r.numPassaggi()==0)
mList.add(r.getDisplayCode());
}
return mList;
}
//private void mergeRoute
+
+ /// ------- Parcelable stuff ---
+ protected Palina(Parcel in) {
+ super(in);
+ routes = in.createTypedArrayList(Route.CREATOR);
+ routesModified = in.readByte() != 0;
+ allSource = in.readByte() == 0 ? null : Passaggio.Source.valueOf(in.readString());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeTypedList(routes);
+ dest.writeByte((byte) (routesModified ? 1 : 0));
+ if (allSource == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ dest.writeString(allSource.name());
+ }
+ }
+
+ public static final Creator<Palina> CREATOR = new Creator<Palina>() {
+ @Override
+ public Palina createFromParcel(Parcel in) {
+ return new Palina(in);
+ }
+
+ @Override
+ public Palina[] newArray(int size) {
+ return new Palina[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+ // Methods using the parcelable
+ public byte[] asByteArray(){
+ final Parcel p = Parcel.obtain();
+ writeToParcel(p,0);
+ final byte[] b = p.marshall();
+ p.recycle();
+ return b;
+ }
+
+ public static Palina fromByteArray(byte[] data){
+ final Parcel p = Parcel.obtain();
+ p.unmarshall(data, 0, data.length);
+ p.setDataPosition(0);
+ final Palina palina = Palina.CREATOR.createFromParcel(p);
+ p.recycle();
+ return palina;
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java b/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java
index baddc8d..ae4b734 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java
@@ -1,189 +1,236 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.backend;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import java.util.Locale;
-public final class Passaggio implements Comparable<Passaggio> {
+public final class Passaggio implements Comparable<Passaggio>, Parcelable {
private static final int UNKNOWN_TIME = -3;
private static final String DEBUG_TAG = "BusTO-Passaggio";
private final String passaggioGTT;
public final int hh,mm;
private @Nullable Integer realtimeDifference;
public final boolean isInRealTime;
public final Source source;
/**
* Useless constructor.
*
* //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
*/
// public Passaggio(@NonNull String TimeGTT) {
// this.passaggio = TimeGTT;
// }
@Override
public String toString() {
return this.passaggioGTT;
}
/**
* Constructs a time (passaggio) for the timetable.
*
* @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
* @throws IllegalArgumentException if nothing reasonable can be extracted from the string
*/
public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) {
passaggioGTT = TimeGTT;
source = sorgente;
String[] parts = TimeGTT.split(":");
String hh,mm;
boolean realtime;
if(parts.length != 2) {
//throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
this.hh = UNKNOWN_TIME;
this.mm = UNKNOWN_TIME;
this.isInRealTime = false;
return;
}
hh = parts[0];
if(parts[1].endsWith("*")) {
mm = parts[1].substring(0, parts[1].length() - 1);
realtime = true;
} else {
mm = parts[1];
realtime = false;
}
int hour=-3,min=-3;
try {
hour = Integer.parseInt(hh);
min = Integer.parseInt(mm);
} catch (NumberFormatException ex){
Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes");
hour = UNKNOWN_TIME;
min = UNKNOWN_TIME;
realtime = false;
} finally {
this.hh = hour;
this.mm = min;
this.isInRealTime = realtime;
}
}
public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){
this.hh = hour;
this.mm = minutes;
this.isInRealTime = realtime;
if (!realtime) realtimeDifference = 0;
this.source = sorgente;
//Build the passaggio string
StringBuilder sb = new StringBuilder();
sb.append(hour).append(":").append(minutes);
if(realtime) sb.append("*");
this.passaggioGTT = sb.toString();
}
public static String createPassaggioGTT(String timeInput, boolean realtime){
final String time = timeInput.trim();
if(time.contains("*")){
if(realtime) return time;
else return time.substring(0,time.length()-1);
} else{
if(realtime) return time.concat("*");
else return time;
}
}
public Passaggio(int numSeconds, boolean realtime, int timeDiff, Source source){
int minutes = numSeconds / 60;
int hours = minutes / 60;
//this.hh = hours;
this.mm = minutes - hours*60;
this.hh = hours % 24;
this.realtimeDifference = timeDiff/60;
this.isInRealTime = realtime;
this.source = source;
this.passaggioGTT = makePassaggioGTT(this.hh, this.mm, this.isInRealTime);
}
private static String makePassaggioGTT(int hour, int minutes, boolean realtime){
StringBuilder sb = new StringBuilder();
sb.append(String.format(Locale.ITALIAN,"%02d", hour)).append(":").append(String.format(Locale.ITALIAN,"%02d", minutes));
if(realtime) sb.append("*");
return sb.toString();
}
@Override
public int compareTo(@NonNull Passaggio other) {
if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME)
return 0;
else {
int diff = getMinutesDiff(other);
// we should take into account if one is in real time and the other isn't, shouldn't we?
if (other.isInRealTime) {
diff+=2;
}
if (this.isInRealTime) {
diff -=2;
}
return diff;
}
}
public int getMinutesDiff(Passaggio other){
int diff = this.hh - other.hh;
// an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01)
if (diff > 12) { // untested
diff -= 24;
} else if (diff < -12) {
diff += 24;
}
diff *= 60;
diff += this.mm - other.mm;
return diff;
}
+ protected Passaggio(Parcel in) {
+ passaggioGTT = in.readString();
+ hh = in.readInt();
+ mm = in.readInt();
+ if (in.readByte() == 0) {
+ realtimeDifference = null;
+ } else {
+ realtimeDifference = in.readInt();
+ }
+ isInRealTime = in.readByte() != 0;
+ source = Source.valueOf(in.readString());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(passaggioGTT);
+ dest.writeInt(hh);
+ dest.writeInt(mm);
+ if (realtimeDifference == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ dest.writeInt(realtimeDifference);
+ }
+ dest.writeByte((byte) (isInRealTime ? 1 : 0));
+ dest.writeString(source.name());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<Passaggio> CREATOR = new Creator<Passaggio>() {
+ @Override
+ public Passaggio createFromParcel(Parcel in) {
+ return new Passaggio(in);
+ }
+
+ @Override
+ public Passaggio[] newArray(int size) {
+ return new Passaggio[size];
+ }
+ };
+
//
// @Override
// public String toString() {
// String resultString = (this.hh).concat(":").concat(this.mm);
// if(this.isInRealTime) {
// return resultString.concat("*");
// } else {
// return resultString;
// }
// }
public enum Source{
FiveTAPI,GTTJSON,FiveTScraper,MatoAPI, UNDETERMINED
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Route.java b/app/src/main/java/it/reyboz/bustorino/backend/Route.java
index ad6722d..4628d61 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Route.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Route.java
@@ -1,446 +1,501 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.backend;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
-public class Route implements Comparable<Route> {
+public class Route implements Comparable<Route>, Parcelable {
final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY};
- final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
+ //final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY};
private final static int BRANCHID_MISSING = -1;
private final String name;
private @Nullable String displayCode = null;
public String destinazione;
public final List<Passaggio> passaggi;
//create a copy of the list, so that
private List<Passaggio> sortedPassaggi;
public final Type type;
public String description;
//ordered list of stops, from beginning to end of line
private List<String> stopsList = null;
public int branchid = BRANCHID_MISSING;
public int[] serviceDays ={};
//0=>feriale, 1=>festivo -2=>unknown
public FestiveInfo festivo = FestiveInfo.UNKNOWN;
private @Nullable String gtfsId;
public enum Type { // "long distance" sono gli extraurbani.
BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2);
//TODO: decide to give some special parameter to each field
private final int code;
Type(int code){
this.code = code;
}
public int getCode(){
return this.code;
}
@Nullable
public static Type fromCode(int i){
- switch (i){
- case 1:
- return BUS;
- case 2:
- return LONG_DISTANCE_BUS;
- case 3:
- return METRO;
- case 4:
- return RAILWAY;
- case 5:
- return TRAM;
- case -2:
- return UNKNOWN;
- default:
- return null;
- }
+ return switch (i) {
+ case 1 -> BUS;
+ case 2 -> LONG_DISTANCE_BUS;
+ case 3 -> METRO;
+ case 4 -> RAILWAY;
+ case 5 -> TRAM;
+ case -2 -> UNKNOWN;
+ default -> null;
+ };
}
}
public enum FestiveInfo{
FESTIVO(1),FERIALE(0),UNKNOWN(-2);
private final int code;
FestiveInfo(int code){
this.code = code;
}
public int getCode() {
return code;
}
public static FestiveInfo fromCode(int i){
- switch (i){
- case -2:
- return UNKNOWN;
- case 0:
- return FERIALE;
- case 1:
- return FESTIVO;
- default:
- return UNKNOWN;
- }
+ return switch (i) {
+ case -2 -> UNKNOWN;
+ case 0 -> FERIALE;
+ case 1 -> FESTIVO;
+ default -> UNKNOWN;
+ };
}
}
/**
* Constructor.
*
* @param name route ID
* @param destinazione terminus\end of line
* @param type bus, long distance bus, underground, and so on
* @param passaggi timetable, a good choice is an ArrayList of size 6
* @param description the description of the line, usually given by the FiveTAPIFetcher
* @see Palina Palina.addRoute() method
*/
public Route(String name, String destinazione, List<Passaggio> passaggi, Type type, String description) {
this.name = name;
this.destinazione = parseDestinazione(destinazione);
this.passaggi = passaggi;
this.type = type;
this.description = description;
}
/**
* Constructor used in GTTJSONFetcher, see above
*/
public Route(String name, String destinazione, Type type, List<Passaggio> passaggi) {
this(name,destinazione,passaggi,type,null);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,Type t,String description){
this(name,null,new ArrayList<>(),t,description);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,String destinazione, String description, Type t){
this(name,destinazione,new ArrayList<>(),t,description);
}
/**
* Exactly what it says on the tin.
*
* @return times from the timetable
*/
public List<Passaggio> getPassaggi() {
return this.passaggi;
}
public void setStopsList(List<String> stopsList) {
this.stopsList = Collections.unmodifiableList(stopsList);
}
public List<String> getStopsList(){
return this.stopsList;
}
/**
* Adds a time (passaggio) to the timetable for this route
*
* @param TimeGTT time in GTT format (e.g. "11:22*")
*/
public void addPassaggio(String TimeGTT, Passaggio.Source source) {
this.passaggi.add(new Passaggio(TimeGTT, source));
}
//Overloaded
public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
this.passaggi.add(new Passaggio(hour, minutes, realtime, source));
}
public static Route.Type getTypeFromSymbol(String route) {
switch (route) {
case "M":
return Route.Type.METRO;
case "T":
return Route.Type.RAILWAY;
}
// default with case "B"
return Route.Type.BUS;
}
private String parseDestinazione(String direzione){
if(direzione==null) return null;
//trial to add space to the parenthesis
String[] exploded = direzione.split("\\(");
if(exploded.length>1){
StringBuilder sb = new StringBuilder();
sb.append(exploded[0]);
for(int i=1; i<exploded.length;i++) {
sb.append(" (");
sb.append(exploded[i]);
}
direzione = sb.toString();
}
return direzione;
}
/**
* Getter for the name
* @return the name of the line
*/
public String getName() {
return name;
}
/*public String getNameForDisplay(){
if(name.trim().equals("101Metrobus")) return "101 Metrobus";
else return name;
}
*/
public String getDisplayCode(){
if(displayCode!=null) return displayCode;
displayCode = FiveTNormalizer.fixShortNameForDisplay(name);
return displayCode;
}
/**
* Get all passaggi in a single string
* @return the string
*/
public String getPassaggiToString(){
StringBuilder sb = new StringBuilder();
for(Passaggio passaggio : passaggi) {
// "+" calls concat() and some other stuff internally, this should be faster
//StringBuilder is THE WAY
sb.append(passaggio.toString());
sb.append(" ");
}
return sb.toString();
}
public String getPassaggiToString(int start_idx, int number, boolean sort){
StringBuilder sb = new StringBuilder();
List<Passaggio> arrivals;
int max;
if(sort){
if(sortedPassaggi==null){
sortedPassaggi = new ArrayList<>(passaggi.size());
sortedPassaggi.addAll(passaggi);
Collections.sort(sortedPassaggi);
}
arrivals = sortedPassaggi;
} else arrivals = passaggi;
max = Math.min(start_idx + number, arrivals.size());
for(int j= start_idx; j<max;j++) {
// "+" calls concat() and some other stuff internally, this should be faster
//StringBuilder is THE WAY
sb.append(arrivals.get(j).toString());
sb.append(" ");
}
return sb.toString();
}
public int numPassaggi(){
if (passaggi==null)
return 0;
return passaggi.size();
}
public Passaggio.Source getPassaggiSource(){
Passaggio.Source mSource = null;
for(Passaggio pass: passaggi){
if (mSource == null) {
mSource = pass.source;
} else if (mSource != pass.source){
Log.w("BusTO-CheckPassaggi",
"Cannot determine the source for route "+this.name+", have got "+mSource +" so far, the next one is "+pass.source );
mSource = Passaggio.Source.UNDETERMINED;
break;
}
}
if (mSource == null) mSource = Passaggio.Source.UNDETERMINED;
return mSource;
}
@Override
public int compareTo(@NonNull Route other) {
int res;
int thisAsInt, otherAsInt;
// sorting by numbers alone yields a far more "natural" result (36N goes before 2024, 95B next to 95, and the like)
thisAsInt = networkTools.failsafeParseInt(this.name.replaceAll("[^0-9]", ""));
otherAsInt = networkTools.failsafeParseInt(other.name.replaceAll("[^0-9]", ""));
// compare.
// numeric route IDs (names)
if(thisAsInt != 0 && otherAsInt != 0) {
res = thisAsInt - otherAsInt;
if(res != 0) {
return res;
}
} else {
// non-numeric
res = this.name.compareTo(other.name);
if (res != 0) {
return res;
}
// compare gtfsID
if (this.gtfsId != null && other.gtfsId!=null){
res = this.gtfsId.compareTo(other.gtfsId);
if (res!=0) return 0;
}
}
// try comparing their destination
if(this.destinazione!=null){
res = this.destinazione.compareTo(other.destinazione);
if(res != 0) {
return res;
}
}
//compare the lines
if(this.stopsList!=null && other.stopsList!=null){
int d = this.stopsList.size()-other.stopsList.size();
if(d!=0) return d;
//if we are here, the two routes have the same number of stops
}
// probably useless, but... last attempt.
if(this.type != other.type) {
// ordinal() is evil or whatever, who cares.
return this.type.ordinal() - other.type.ordinal();
}
return 0;
}
@Nullable
public String getGtfsId() {
return gtfsId;
}
public void setGtfsId(@Nullable String gtfsId) {
if (gtfsId==null) this.gtfsId = null;
else
this.gtfsId = gtfsId.trim();
}
public boolean isBranchIdValid(){
return branchid!=BRANCHID_MISSING;
}
@Override
public boolean equals(Object obj) {
- if(obj instanceof Route){
- Route r = (Route) obj;
+ if(obj instanceof Route r){
boolean result = false;
if(this.name.equals(r.name) && this.branchid == r.branchid){
if(description!=null && r.description!=null)
if(!description.trim().equals(r.description.trim()))
return false;
if(destinazione!=null && r.destinazione!=null){
if(!this.destinazione.trim().equals(r.destinazione.trim()))
// they are not the same
return false;
}
if(gtfsId!=null && r.gtfsId!=null && !(gtfsId.trim().equals(r.gtfsId.trim())))
return false;
//check stops list
if(this.stopsList!=null && r.stopsList!=null){
int sizeDiff = this.stopsList.size()-r.stopsList.size();
if(sizeDiff!=0) {
return false;
} else {
//check that the stops are the same
result = true;
for(int j=0; j<this.stopsList.size();j++){
if(!this.stopsList.get(j).equals(r.stopsList.get(j))) {
result = false;
break;
}
}
return result;
}
} else{
//no stopsList in one or the other
return true;
}
}
return result;
} else return false;
}
/**
* Merge informations from another route
* NO CONSISTENCY CHECKS, DO BEFORE CALLING THIS METHOD
* @param other the other route
* @return true if there have been changes
*/
public boolean mergeRouteWithAnother(Route other){
boolean adjusted = false;
if ((other.serviceDays!=null && this.serviceDays!=null && this.serviceDays.length==0)
|| (other.serviceDays!=null && this.serviceDays==null)) {
this.serviceDays = other.serviceDays;
adjusted = true;
}
if (other.getStopsList() != null && this.getStopsList() == null)
this.setStopsList(other.getStopsList());
if(this.passaggi!=null && other.passaggi!=null && other.passaggi.size()>0){
this.passaggi.addAll(other.passaggi);
}
if(this.destinazione == null && other.destinazione!=null) {
this.destinazione = other.destinazione;
adjusted = true;
}
if(!this.isBranchIdValid() && other.isBranchIdValid()) {
this.branchid = other.branchid;
adjusted = true;
}
if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){
this.festivo = other.festivo;
adjusted = true;
}
if(other.description!=null && (this.description==null ||
(this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) ||
(this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) {
this.description = other.description;
}
return adjusted;
}
+ // ---- Parcelable implem ---
+ protected Route(Parcel in) {
+ name = in.readString();
+ displayCode = in.readByte() == 0 ? null : in.readString();
+ destinazione = in.readString();
+ passaggi = in.createTypedArrayList(Passaggio.CREATOR);
+ type = Type.valueOf(in.readString());
+ description = in.readString();
+ if (in.readByte() == 0) {
+ stopsList = null;
+ } else {
+ stopsList = in.createStringArrayList();
+ }
+ branchid = in.readInt();
+ serviceDays = in.createIntArray();
+ festivo = FestiveInfo.valueOf(in.readString());
+ gtfsId = in.readByte() == 0 ? null : in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ if (displayCode == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ dest.writeString(displayCode);
+ }
+ dest.writeString(destinazione);
+ dest.writeTypedList(passaggi);
+ dest.writeString(type.name());
+ dest.writeString(description);
+ if (stopsList == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ dest.writeStringList(stopsList);
+ }
+ dest.writeInt(branchid);
+ dest.writeIntArray(serviceDays);
+ dest.writeString(festivo.name());
+ if (gtfsId == null) {
+ dest.writeByte((byte) 0);
+ } else {
+ dest.writeByte((byte) 1);
+ dest.writeString(gtfsId);
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Route> CREATOR = new Creator<>() {
+ @Override
+ public Route createFromParcel(Parcel in) {
+ return new Route(in);
+ }
+
+ @Override
+ public Route[] newArray(int size) {
+ return new Route[size];
+ }
+ };
}
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 b7547ad..2ae77a7 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Stop.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Stop.java
@@ -1,360 +1,429 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.backend;
import android.location.Location;
import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import it.reyboz.bustorino.util.LinesNameSorter;
import org.jetbrains.annotations.NotNull;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
-public class Stop implements Comparable<Stop> {
+public class Stop implements Comparable<Stop>, Parcelable {
// 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<String> 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<String> 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<String> 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<String> routesThatStopHere) {
this.routesThatStopHere = routesThatStopHere;
}
protected void setRoutesThatStopHereString(String routesStopping){
this.routesThatStopHereString = routesStopping;
}
public int getNumRoutesStopping(){
if(this.routesThatStopHere == null) {
return 0;
} else {
return this.routesThatStopHere.size();
}
}
@Nullable
protected List<String> 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).<br>
* - empty string means "already searched everywhere, can't find it"<br>
* - null means "didn't search, yet. Maybe you should try."<br>
* - string means "here's the name.", obviously.<br>
*
* @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.<br>
* 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.<br>
* 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;
}
@Override
public String toString(){
return "id:"+ID+" { name: "+this.name+", lines: "+this.routesThatStopHereToString()+"}";
}
@Nullable
public Double getLatitude() {
return lat;
}
@Nullable
public Double getLongitude() {
return lon;
}
public Double getDistanceFromLocation(@NotNull GPSPoint 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,latitude, longitude);
else return Double.POSITIVE_INFINITY;
}
public Bundle toBundle(Bundle bundle) {
//Bundle bundle = new Bundle();
if(bundle==null) return null;
bundle.putString("ID", ID);
bundle.putString("name", name);
bundle.putString("username", username);
bundle.putString("location", location);
bundle.putString("type", (type != null) ? type.name() : null);
bundle.putStringArrayList("routesThatStopHere", (routesThatStopHere != null) ? new ArrayList<>(routesThatStopHere) : null);
if (lat != null) bundle.putDouble("lat", lat);
if (lon != null) bundle.putDouble("lon", lon);
if (gtfsID !=null) bundle.putString("gtfsID", gtfsID);
return bundle;
}
public Bundle toBundle(){
return toBundle(new Bundle());
}
@Nullable
public static Stop fromBundle(Bundle bundle) {
String ID = bundle.getString("ID");
if (ID == null) return null; //throw new IllegalArgumentException("ID cannot be null");
String name = bundle.getString("name");
String username = bundle.getString("username");
String location = bundle.getString("location");
String typeStr = bundle.getString("type");
Route.Type type = (typeStr != null) ? Route.Type.valueOf(typeStr) : null;
List<String> routesThatStopHere = bundle.getStringArrayList("routesThatStopHere");
Double lat = bundle.containsKey("lat") ? bundle.getDouble("lat") : null;
Double lon = bundle.containsKey("lon") ? bundle.getDouble("lon") : null;
String gtfsId = bundle.getString("gtfsID");
return new Stop(ID, name, username, location, type, routesThatStopHere, lat, lon, gtfsId);
}
+
+ /// ----- Parcelable implementation ----
+ protected Stop(Parcel in) {
+ ID = in.readString();
+ name = in.readByte() == 0 ? null : in.readString();
+ username = in.readByte() == 0 ? null : in.readString();
+ location = in.readByte() == 0 ? null : in.readString();
+ type = in.readByte() == 0 ? null : Route.Type.valueOf(in.readString());
+ routesThatStopHere = in.readByte() == 0 ? null : in.createStringArrayList();
+ lat = in.readByte() == 0 ? null : in.readDouble();
+ lon = in.readByte() == 0 ? null : in.readDouble();
+ routesThatStopHereString = in.readByte() == 0 ? null : in.readString();
+ absurdGTTPlaceName = in.readByte() == 0 ? null : in.readString();
+ gtfsID = in.readByte() == 0 ? null : in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(ID);
+ dest.writeByte((byte) (name == null ? 0 : 1));
+ if (name != null) dest.writeString(name);
+
+ dest.writeByte((byte) (username == null ? 0 : 1));
+ if (username != null) dest.writeString(username);
+
+ dest.writeByte((byte) (location == null ? 0 : 1));
+ if (location != null) dest.writeString(location);
+
+ dest.writeByte((byte) (type == null ? 0 : 1));
+ if (type != null) dest.writeString(type.name());
+
+ dest.writeByte((byte) (routesThatStopHere == null ? 0 : 1));
+ if (routesThatStopHere != null) dest.writeStringList(routesThatStopHere);
+
+ dest.writeByte((byte) (lat == null ? 0 : 1));
+ if (lat != null) dest.writeDouble(lat);
+
+ dest.writeByte((byte) (lon == null ? 0 : 1));
+ if (lon != null) dest.writeDouble(lon);
+
+ dest.writeByte((byte) (routesThatStopHereString == null ? 0 : 1));
+ if (routesThatStopHereString != null) dest.writeString(routesThatStopHereString);
+
+ dest.writeByte((byte) (absurdGTTPlaceName == null ? 0 : 1));
+ if (absurdGTTPlaceName != null) dest.writeString(absurdGTTPlaceName);
+
+ dest.writeByte((byte) (gtfsID == null ? 0 : 1));
+ if (gtfsID != null) dest.writeString(gtfsID);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<Stop> CREATOR = new Creator<>() {
+ @Override
+ public Stop createFromParcel(Parcel in) {
+ return new Stop(in);
+ }
+
+ @Override
+ public Stop[] newArray(int size) {
+ return new Stop[size];
+ }
+ };
+
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
index 57ad63d..8a88895 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/FavoritesLiveData.java
@@ -1,226 +1,227 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.data;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.BuildConfig;
import it.reyboz.bustorino.backend.Stop;
public class FavoritesLiveData extends LiveData<List<Stop>> implements CustomAsyncQueryHandler.AsyncQueryListener {
private static final String TAG = "BusTO-FavoritesLiveData";
private final boolean notifyChangesDescendants;
@NonNull
private final Context mContext;
@NonNull
private final FavoritesLiveData.ForceLoadContentObserver mObserver;
private final CustomAsyncQueryHandler queryHandler;
private final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
AppDataProvider.FAVORITES).build();
private final int FAV_TOKEN = 23, STOPS_TOKEN_BASE=220;
@Nullable
private List<Stop> stopsFromFavorites, stopsDone;
private boolean isQueryRunning = false;
private int stopNeededCount = 0;
public FavoritesLiveData(@NonNull Context context, boolean notifyDescendantsChanges) {
super();
mContext = context.getApplicationContext();
mObserver = new FavoritesLiveData.ForceLoadContentObserver();
notifyChangesDescendants = notifyDescendantsChanges;
queryHandler = new CustomAsyncQueryHandler(mContext.getContentResolver(),this);
}
private void loadData() {
loadData(false);
}
private static Uri.Builder getStopsBuilder(){
return AppDataProvider.getUriBuilderToComplete().appendPath("stop");
}
private void loadData(boolean forceQuery) {
Log.d(TAG, "loadData() force: "+forceQuery);
if (!forceQuery){
if (getValue()!= null){
//Data already loaded
Log.d(TAG, "Data already loaded");
return;
}
}
if (isQueryRunning){
//we are waiting for data, we will get an update soon
Log.d(TAG, "Query is running, abort");
return;
}
isQueryRunning = true;
queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null);
}
public void forceReload(){
loadData(true);
}
@Override
protected void onActive() {
//Log.d(TAG, "onActive()");
loadData(true);
}
/**
* Clear the data for the cursor
*/
public void onClear(){
ContentResolver resolver = mContext.getContentResolver();
resolver.unregisterContentObserver(mObserver);
}
@Override
protected void setValue(List<Stop> stops) {
//Log.d("BusTO-FavoritesLiveData","Setting the new values for the stops, have "+
// stops.size()+" stops");
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(FAVORITES_URI, notifyChangesDescendants,mObserver);
super.setValue(stops);
}
@Override
public void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (cursor == null){
//Nothing to do
Log.e(TAG, "Null cursor for token "+token);
return;
}
if (token == FAV_TOKEN) {
stopsFromFavorites = UserDB.getFavoritesFromCursor(cursor, UserDB.getFavoritesColumnNamesAsArray);
cursor.close();
//reset counters
stopNeededCount = stopsFromFavorites.size();
stopsDone = new ArrayList<>();
if(stopsFromFavorites.size() == 0){
//we don't need to call the other query
setValue(stopsDone);
isQueryRunning = false;
} else
for (int i = 0; i < stopsFromFavorites.size(); i++) {
Stop s = stopsFromFavorites.get(i);
queryHandler.startQuery(STOPS_TOKEN_BASE + i, null,
getStopsBuilder().appendPath(s.ID).build(),
NextGenDB.QUERY_COLUMN_stops_all, null, null, null);
}
} else if(token >= STOPS_TOKEN_BASE){
final int index = token - STOPS_TOKEN_BASE;
assert stopsFromFavorites != null;
Stop stopUpdate = stopsFromFavorites.get(index);
Stop finalStop;
List<Stop> result = NextGenDB.getStopsFromCursorAllFields(cursor);
cursor.close();
- if (result.size() < 1){
+ if (result.isEmpty()){
// stop is not in the DB
finalStop = stopUpdate;
} else {
finalStop = result.get(0);
if (BuildConfig.DEBUG && !(finalStop.ID.equals(stopUpdate.ID))) {
throw new AssertionError("Assertion failed");
}
finalStop.setStopUserName(stopUpdate.getStopUserName());
}
if (stopsDone!=null)
stopsDone.add(finalStop);
stopNeededCount--;
if (stopNeededCount == 0) {
// we have finished the queries
isQueryRunning = false;
Collections.sort(stopsDone);
setValue(stopsDone);
}
}
}
/**
* Content Observer that forces reload of cursor when data changes
* On different thread (new Handler)
*/
public final class ForceLoadContentObserver
extends ContentObserver {
public ForceLoadContentObserver() {
- super(new Handler());
+ super(new Handler(Looper.myLooper()));
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
Log.d(TAG, "ForceLoadContentObserver.onChange()");
loadData(true);
}
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
index fb07af4..e3dbbef 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/NextGenDB.java
@@ -1,497 +1,582 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.data;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;
+import androidx.annotation.NonNull;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import java.util.*;
import java.util.stream.Collectors;
import static it.reyboz.bustorino.data.NextGenDB.Contract.*;
public class NextGenDB extends SQLiteOpenHelper{
public static final String DATABASE_NAME = "bustodatabase.db";
public static final int DATABASE_VERSION = 3;
public static final String DEBUG_TAG = "NextGenDB-BusTO";
//NO Singleton instance
//private static volatile NextGenDB instance = null;
//Some generating Strings
private static final String SQL_CREATE_LINES_TABLE="CREATE TABLE "+Contract.LinesTable.TABLE_NAME+" ("+
Contract.LinesTable._ID +" INTEGER PRIMARY KEY AUTOINCREMENT, "+ Contract.LinesTable.COLUMN_NAME +" TEXT, "+
Contract.LinesTable.COLUMN_DESCRIPTION +" TEXT, "+Contract.LinesTable.COLUMN_TYPE +" TEXT, "+
"UNIQUE ("+LinesTable.COLUMN_NAME+","+LinesTable.COLUMN_DESCRIPTION+","+LinesTable.COLUMN_TYPE+" ) "+" )";
private static final String SQL_CREATE_BRANCH_TABLE="CREATE TABLE "+Contract.BranchesTable.TABLE_NAME+" ("+
Contract.BranchesTable._ID +" INTEGER, "+ Contract.BranchesTable.COL_BRANCHID +" INTEGER PRIMARY KEY, "+
Contract.BranchesTable.COL_LINE +" INTEGER, "+ Contract.BranchesTable.COL_DESCRIPTION +" TEXT, "+
Contract.BranchesTable.COL_DIRECTION+" TEXT, "+ Contract.BranchesTable.COL_TYPE +" INTEGER, "+
//SERVICE DAYS: 0 => FERIALE,1=>FESTIVO,-1=>UNKNOWN,add others if necessary
Contract.BranchesTable.COL_FESTIVO +" INTEGER, "+
//DAYS COLUMNS. IT'S SO TEDIOUS I TRIED TO KILL MYSELF
BranchesTable.COL_LUN+" INTEGER, "+BranchesTable.COL_MAR+" INTEGER, "+BranchesTable.COL_MER+" INTEGER, "+BranchesTable.COL_GIO+" INTEGER, "+
BranchesTable.COL_VEN+" INTEGER, "+ BranchesTable.COL_SAB+" INTEGER, "+BranchesTable.COL_DOM+" INTEGER, "+
"FOREIGN KEY("+ Contract.BranchesTable.COL_LINE +") references "+ Contract.LinesTable.TABLE_NAME+"("+ Contract.LinesTable._ID+") "
+")";
private static final String SQL_CREATE_CONNECTIONS_TABLE="CREATE TABLE "+Contract.ConnectionsTable.TABLE_NAME+" ("+
Contract.ConnectionsTable.COLUMN_BRANCH+" INTEGER, "+ Contract.ConnectionsTable.COLUMN_STOP_ID+" TEXT, "+
Contract.ConnectionsTable.COLUMN_ORDER+" INTEGER, "+
"PRIMARY KEY ("+ Contract.ConnectionsTable.COLUMN_BRANCH+","+ Contract.ConnectionsTable.COLUMN_STOP_ID + "), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_BRANCH+") references "+ Contract.BranchesTable.TABLE_NAME+"("+ Contract.BranchesTable.COL_BRANCHID +"), "+
"FOREIGN KEY("+ Contract.ConnectionsTable.COLUMN_STOP_ID+") references "+ Contract.StopsTable.TABLE_NAME+"("+ Contract.StopsTable.COL_ID +") "
+")";
private static final String SQL_CREATE_STOPS_TABLE="CREATE TABLE "+Contract.StopsTable.TABLE_NAME+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
StopsTable.COL_GTFS_ID+" TEXT, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
private static final String SQL_CREATE_STOPS_TABLE_TO_COMPLETE = " ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
public static final String[] QUERY_COLUMN_stops_all = {
StopsTable.COL_ID, StopsTable.COL_NAME, StopsTable.COL_GTFS_ID, StopsTable.COL_LOCATION,
StopsTable.COL_TYPE, StopsTable.COL_LAT, StopsTable.COL_LONG, StopsTable.COL_LINES_STOPPING};
public static final String QUERY_WHERE_LAT_AND_LNG_IN_RANGE = StopsTable.COL_LAT + " >= ? AND " +
StopsTable.COL_LAT + " <= ? AND "+ StopsTable.COL_LONG +
" >= ? AND "+ StopsTable.COL_LONG + " <= ?";
public static final String QUERY_FROM_GTFS_ID_IN_TO_COMPLETE= StopsTable.COL_GTFS_ID +" IN ";
public static String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?";
private final Context appContext;
private static NextGenDB INSTANCE;
private NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
public static NextGenDB getInstance(Context context) {
if (INSTANCE == null){
INSTANCE = new NextGenDB(context);
}
return INSTANCE;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d("BusTO-AppDB","Lines creating database:\n"+SQL_CREATE_LINES_TABLE+"\n"+
SQL_CREATE_STOPS_TABLE+"\n"+SQL_CREATE_BRANCH_TABLE+"\n"+SQL_CREATE_CONNECTIONS_TABLE);
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(oldVersion<2 && newVersion == 2){
//DROP ALL TABLES
db.execSQL("DROP TABLE "+ConnectionsTable.TABLE_NAME);
db.execSQL("DROP TABLE "+BranchesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+LinesTable.TABLE_NAME);
db.execSQL("DROP TABLE "+ StopsTable.TABLE_NAME);
//RECREATE THE TABLES WITH THE NEW SCHEMA
db.execSQL(SQL_CREATE_LINES_TABLE);
db.execSQL(SQL_CREATE_STOPS_TABLE);
//tables with constraints
db.execSQL(SQL_CREATE_BRANCH_TABLE);
db.execSQL(SQL_CREATE_CONNECTIONS_TABLE);
DatabaseUpdate.requestDBUpdateWithWork(appContext, true, true);
}
if(oldVersion < 3 && newVersion == 3){
Log.d("BusTO-Database", "Running upgrades for version 3");
//add the new column
db.execSQL("ALTER TABLE "+StopsTable.TABLE_NAME+
" ADD COLUMN "+StopsTable.COL_GTFS_ID+" TEXT ");
// DatabaseUpdate.requestDBUpdateWithWork(appContext, true);
}
}
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
db.execSQL("PRAGMA foreign_keys=ON");
}
public static String getSqlCreateStopsTable(String tableName){
return "CREATE TABLE "+tableName+" ("+
Contract.StopsTable.COL_ID+" TEXT PRIMARY KEY, "+ Contract.StopsTable.COL_TYPE+" INTEGER, "+Contract.StopsTable.COL_LAT+" REAL NOT NULL, "+
Contract.StopsTable.COL_LONG+" REAL NOT NULL, "+ Contract.StopsTable.COL_NAME+" TEXT NOT NULL, "+
Contract.StopsTable.COL_LOCATION+" TEXT, "+Contract.StopsTable.COL_PLACE+" TEXT, "+
Contract.StopsTable.COL_LINES_STOPPING +" TEXT )";
}
/**
* Query some bus stops inside a map view
* @return stoplist, if empty it means that an error occurred
*
* You can obtain the coordinates from OSMDroid using something like this:
* BoundingBoxE6 bb = mMapView.getBoundingBox();
* double latFrom = bb.getLatSouthE6() / 1E6;
* double latTo = bb.getLatNorthE6() / 1E6;
* double lngFrom = bb.getLonWestE6() / 1E6;
* double lngTo = bb.getLonEastE6() / 1E6;
*/
public synchronized ArrayList<Stop> queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
ArrayList<Stop> stops = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
// coordinates must be strings in the where condition
String minLatRaw = String.valueOf(minLat);
String maxLatRaw = String.valueOf(maxLat);
String minLngRaw = String.valueOf(minLng);
String maxLngRaw = String.valueOf(maxLng);
if(db == null) {
return stops;
}
try {
final Cursor result = db.query(StopsTable.TABLE_NAME, QUERY_COLUMN_stops_all, QUERY_WHERE_LAT_AND_LNG_IN_RANGE,
new String[] {minLatRaw, maxLatRaw, minLngRaw, maxLngRaw},
null, null, null);
stops = getStopsFromCursorAllFields(result);
result.close();
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
return stops;
}catch (Exception e){
Log.e(DEBUG_TAG, "Exception occurred when getting stops");
e.printStackTrace();
return stops;
}
finally {
db.close();
}
return stops;
}
/**
* Query stops in the database having these IDs
* REMEMBER TO CLOSE THE DB CONNECTION AFTERWARDS
* @param bustoDB readable database instance
* @param gtfsIDs gtfs IDs to query
* @return list of stops
*/
public static synchronized ArrayList<Stop> queryAllStopsWithGtfsIDs(SQLiteDatabase bustoDB, List<String> gtfsIDs){
final ArrayList<Stop> stops = new ArrayList<>();
if(bustoDB == null){
Log.e(DEBUG_TAG, "Asked query for IDs but database is null");
return stops;
} else if (gtfsIDs == null || gtfsIDs.isEmpty()) {
return stops;
}
final StringBuilder builder = new StringBuilder(QUERY_FROM_GTFS_ID_IN_TO_COMPLETE);
boolean first = true;
builder.append(" ( ");
for(int i=0; i< gtfsIDs.size(); i++){
if(first){
first = false;
} else{
builder.append(", ");
}
builder.append("?");//.append("\"").append(id).append("\"");
}
builder.append(") ");
final String whereClause = builder.toString();
final String[] idsQuery = gtfsIDs.toArray(new String[0]);
try {
final Cursor result = bustoDB.query(StopsTable.TABLE_NAME,QUERY_COLUMN_stops_all, whereClause,
idsQuery,
null, null, null);
stops.addAll(getStopsFromCursorAllFields(result));
result.close();
} catch(SQLiteException e) {
Log.e(DEBUG_TAG, "SQLiteException occurred");
e.printStackTrace();
}
return stops;
}
/**
* Get the list of stop in the query, with all the possible fields {NextGenDB.QUERY_COLUMN_stops_all}
* @param result cursor from query
* @return an Array of the stops found in the query
*/
public static ArrayList<Stop> getStopsFromCursorAllFields(Cursor result){
final int colID = result.getColumnIndex(StopsTable.COL_ID);
final int colName = result.getColumnIndex(StopsTable.COL_NAME);
final int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
final int colType = result.getColumnIndex(StopsTable.COL_TYPE);
final int colLat = result.getColumnIndex(StopsTable.COL_LAT);
final int colGtfsID = result.getColumnIndex(StopsTable.COL_GTFS_ID);
final int colLon = result.getColumnIndex(StopsTable.COL_LONG);
final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
int count = result.getCount();
ArrayList<Stop> stops = new ArrayList<>(count);
int i = 0;
while(result.moveToNext()) {
final String stopID = result.getString(colID).trim();
Route.Type type;
//if(result.getString(colType) == null) type = Route.Type.BUS;
//else type = Route.getTypeFromSymbol(result.getString(colType));
//if(result.getInt(colType) == null) type = Route.Type.BUS;
try{
type = Route.Type.fromCode(result.getInt(colType));
} catch (Exception e){
type = Route.Type.BUS;
}
String lines = result.getString(colLines).trim();
String locationSometimesEmpty = result.getString(colLocation);
if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
locationSometimesEmpty = null;
}
stops.add(new Stop(stopID, result.getString(colName), null,
locationSometimesEmpty, type, splitLinesString(lines),
result.getDouble(colLat), result.getDouble(colLon),
result.getString(colGtfsID))
);
}
return stops;
}
public static synchronized int writeLinesStoppingHere(SQLiteDatabase db, HashMap<String,Set<String>> linesStoppingBy){
int rowsUpdated = 0;
for (String stopGtfsID : linesStoppingBy.keySet()){
if (linesStoppingBy.get(stopGtfsID)==null) continue;
if (linesStoppingBy.get(stopGtfsID).isEmpty()) continue;
ArrayList<String> ll = new ArrayList<>(linesStoppingBy.get(stopGtfsID));
String stringForStops = Palina.buildRoutesStringFromNames(ll);
ContentValues cv = new ContentValues();
cv.put(StopsTable.COL_LINES_STOPPING, stringForStops);
// Which row to update, based on the title
String selection = StopsTable.COL_GTFS_ID + " LIKE ?";
String[] selectionArgs = { stopGtfsID };
int count = db.update(
StopsTable.TABLE_NAME,
cv,
selection,
selectionArgs);
if (count > 1){
Log.e(DEBUG_TAG, "Updated the linesStoppingBy for more than one stop");
}
rowsUpdated += count;
}
return rowsUpdated;
}
+
+ public static boolean insertBranchesIntoDB(@NonNull Context context, @NonNull List<Route> routesToInsert){
+ final NextGenDB nextGenDB = NextGenDB.getInstance(context);
+ //ContentValues[] values = new ContentValues[routesToInsert.size()];
+ ArrayList<ContentValues> branchesValues = new ArrayList<>(routesToInsert.size());
+ ArrayList<ContentValues> connectionsVals = new ArrayList<>(routesToInsert.size());
+ long starttime,endtime;
+ for (Route r:routesToInsert){
+ //if it has received an interrupt, stop
+ if(Thread.interrupted()) return false;
+ //otherwise, build contentValues
+ final ContentValues cv = new ContentValues();
+ cv.put(BranchesTable.COL_BRANCHID,r.branchid);
+ cv.put(LinesTable.COLUMN_NAME,r.getName());
+ cv.put(BranchesTable.COL_DIRECTION,r.destinazione);
+ cv.put(BranchesTable.COL_DESCRIPTION,r.description);
+ for (int day :r.serviceDays) {
+ switch (day){
+ case Calendar.MONDAY:
+ cv.put(BranchesTable.COL_LUN,1);
+ break;
+ case Calendar.TUESDAY:
+ cv.put(BranchesTable.COL_MAR,1);
+ break;
+ case Calendar.WEDNESDAY:
+ cv.put(BranchesTable.COL_MER,1);
+ break;
+ case Calendar.THURSDAY:
+ cv.put(BranchesTable.COL_GIO,1);
+ break;
+ case Calendar.FRIDAY:
+ cv.put(BranchesTable.COL_VEN,1);
+ break;
+ case Calendar.SATURDAY:
+ cv.put(BranchesTable.COL_SAB,1);
+ break;
+ case Calendar.SUNDAY:
+ cv.put(BranchesTable.COL_DOM,1);
+ break;
+ }
+ }
+ if(r.type!=null) cv.put(BranchesTable.COL_TYPE, r.type.getCode());
+ cv.put(BranchesTable.COL_FESTIVO, r.festivo.getCode());
+
+ //values[routesToInsert.indexOf(r)] = cv;
+ branchesValues.add(cv);
+ if(r.getStopsList() != null)
+ for(int i=0; i<r.getStopsList().size();i++){
+ String stop = r.getStopsList().get(i);
+ final ContentValues connVal = new ContentValues();
+ connVal.put(ConnectionsTable.COLUMN_STOP_ID,stop);
+ connVal.put(ConnectionsTable.COLUMN_ORDER,i);
+ connVal.put(ConnectionsTable.COLUMN_BRANCH,r.branchid);
+
+ //add to global connVals
+ connectionsVals.add(connVal);
+ }
+ }
+ starttime = System.currentTimeMillis();
+ ContentResolver cr = context.getContentResolver();
+ try {
+ cr.bulkInsert(Uri.parse("content://" + AppDataProvider.AUTHORITY + "/branches/"), branchesValues.toArray(new ContentValues[0]));
+ endtime = System.currentTimeMillis();
+ Log.d("DataDownload", "Inserted branches, took " + (endtime - starttime) + " ms");
+ } catch (SQLException exc){
+ Log.e("AsyncDataDownload","Inserting data: some error happened, aborting the database insert");
+ exc.printStackTrace();
+ return false;
+ }
+
+ if (!connectionsVals.isEmpty()) {
+ starttime = System.currentTimeMillis();
+ ContentValues[] valArr = connectionsVals.toArray(new ContentValues[0]);
+ Log.d("DataDownloadInsert", "inserting " + valArr.length + " connections");
+ int rows = nextGenDB.insertBatchContent(valArr, ConnectionsTable.TABLE_NAME);
+ endtime = System.currentTimeMillis();
+ Log.d("DataDownload", "Inserted connections found, took " + (endtime - starttime) + " ms, inserted " + rows + " rows");
+ }
+ nextGenDB.close();
+ return true;
+ }
/*
static ArrayList<Stop> createStopListFromCursor(Cursor data){
ArrayList<Stop> stopList = new ArrayList<>();
final int col_id = data.getColumnIndex(StopsTable.COL_ID);
final int latInd = data.getColumnIndex(StopsTable.COL_LAT);
final int lonInd = data.getColumnIndex(StopsTable.COL_LONG);
final int nameindex = data.getColumnIndex(StopsTable.COL_NAME);
final int typeIndex = data.getColumnIndex(StopsTable.COL_TYPE);
final int linesIndex = data.getColumnIndex(StopsTable.COL_LINES_STOPPING);
data.moveToFirst();
for(int i=0; i<data.getCount();i++){
String[] routes = data.getString(linesIndex).split(",");
stopList.add(new Stop(data.getString(col_id),data.getString(nameindex),null,null,
Route.Type.fromCode(data.getInt(typeIndex)),
Arrays.asList(routes), //the routes should be compact, not normalized yet
data.getDouble(latInd),data.getDouble(lonInd)
)
);
//Log.d("NearbyStopsFragment","Got stop with id "+data.getString(col_id)+
//" and name "+data.getString(nameindex));
data.moveToNext();
}
return stopList;
}
*/
/**
* Insert batch content, already prepared as
* @param content ContentValues array
* @return number of lines inserted
*/
public int insertBatchContent(ContentValues[] content,String tableName) throws SQLiteException {
final SQLiteDatabase db = this.getWritableDatabase();
int success = 0;
db.beginTransaction();
for (final ContentValues cv : content) {
try {
db.replaceOrThrow(tableName, null, cv);
success++;
} catch (SQLiteConstraintException d){
Log.w("NextGenDB_Insert","Failed insert with FOREIGN KEY... \n"+d.getMessage());
} catch (Exception e) {
Log.w("NextGenDB_Insert", e);
}
}
db.setTransactionSuccessful();
db.endTransaction();
return success;
}
int updateLinesStoppingInStop(List<Stop> stops){
return 0;
}
public static List<String> splitLinesString(String linesStr){
return Arrays.asList(linesStr.split("\\s*,\\s*"));
}
public static final class Contract{
//Ok, I get it, it really is a pain in the ass..
// But it's the only way to have maintainable code
public interface DataTables {
String getTableName();
String[] getFields();
}
public static final class LinesTable implements BaseColumns, DataTables {
//The fields
public static final String TABLE_NAME = "lines";
public static final String COLUMN_NAME = "line_name";
public static final String COLUMN_DESCRIPTION = "line_description";
public static final String COLUMN_TYPE = "line_bacino";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_NAME,COLUMN_DESCRIPTION,COLUMN_TYPE};
}
}
public static final class BranchesTable implements BaseColumns, DataTables {
public static final String TABLE_NAME = "branches";
public static final String COL_BRANCHID = "branchid";
public static final String COL_LINE = "lineid";
public static final String COL_DESCRIPTION = "branch_description";
public static final String COL_DIRECTION = "branch_direzione";
public static final String COL_FESTIVO = "branch_festivo";
public static final String COL_TYPE = "branch_type";
public static final String COL_LUN="runs_lun";
public static final String COL_MAR="runs_mar";
public static final String COL_MER="runs_mer";
public static final String COL_GIO="runs_gio";
public static final String COL_VEN="runs_ven";
public static final String COL_SAB="runs_sab";
public static final String COL_DOM="runs_dom";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_BRANCHID,COL_LINE,COL_DESCRIPTION,
COL_DIRECTION,COL_FESTIVO,COL_TYPE,
COL_LUN,COL_MAR,COL_MER,COL_GIO,COL_VEN,COL_SAB,COL_DOM
};
}
}
public static final class ConnectionsTable implements DataTables {
public static final String TABLE_NAME = "connections";
public static final String COLUMN_BRANCH = "branchid";
public static final String COLUMN_STOP_ID = "stopid";
public static final String COLUMN_ORDER = "ordine";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COLUMN_STOP_ID,COLUMN_BRANCH,COLUMN_ORDER};
}
}
public static final class StopsTable implements DataTables {
public static final String TABLE_NAME = "stops";
public static final String COL_ID = "stopid"; //integer
public static final String COL_TYPE = "stop_type";
public static final String COL_NAME = "stop_name";
public static final String COL_GTFS_ID = "gtfs_id";
public static final String COL_LAT = "stop_latitude";
public static final String COL_LONG = "stop_longitude";
public static final String COL_LOCATION = "stop_location";
public static final String COL_PLACE = "stop_placeName";
public static final String COL_LINES_STOPPING = "stop_lines";
@Override
public String getTableName() {
return TABLE_NAME;
}
@Override
public String[] getFields() {
return new String[]{COL_ID,COL_TYPE,COL_NAME,COL_GTFS_ID,COL_LAT,COL_LONG,COL_LOCATION,COL_PLACE,COL_LINES_STOPPING};
}
}
}
public static final class DBUpdatingException extends Exception{
public DBUpdatingException(String message) {
super(message);
}
}
}
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 e4defb3..fb1f894 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt
+++ b/app/src/main/java/it/reyboz/bustorino/data/OldDataRepository.kt
@@ -1,84 +1,86 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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) {
+class OldDataRepository(private val executor: Executor,
+ private val nextGenDB: NextGenDB,
+ ) {
constructor(executor: Executor, context: Context): this(executor, NextGenDB.getInstance(context))
fun requestStopsWithGtfsIDs(
gtfsIDs: List<String?>?,
callback: Callback<List<Stop>>
) {
executor.execute {
try {
//final NextGenDB dbHelper = new NextGenDB(context);
val db = nextGenDB.readableDatabase
val stops: List<Stop> = NextGenDB.queryAllStopsWithGtfsIDs(db, gtfsIDs)
//Result<List<Stop>> 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<java.util.ArrayList<Stop>>
){
//Log.d(DEBUG_TAG, "Async Stop Fetcher started working");
executor.execute {
val stops = nextGenDB.queryAllInsideMapView(
latitFrom, latitTo,
longitFrom, longitTo
)
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<ArrayList<Stop>>){
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<T> {
fun onComplete(result: Result<T>)
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java
index a4c1742..7f9908c 100644
--- a/app/src/main/java/it/reyboz/bustorino/data/UserDB.java
+++ b/app/src/main/java/it/reyboz/bustorino/data/UserDB.java
@@ -1,394 +1,397 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
package it.reyboz.bustorino.data;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.util.*;
+import androidx.annotation.Nullable;
import de.siegmar.fastcsv.reader.CloseableIterator;
import de.siegmar.fastcsv.reader.CsvReader;
import de.siegmar.fastcsv.reader.CsvRow;
import de.siegmar.fastcsv.writer.CsvWriter;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsDBInterface;
public class UserDB extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "user.db";
static final String TABLE_NAME = "favorites";
private final Context c; // needed during upgrade
public final static String COL_ID = "ID";
public final static String COL_USERNAME="username";
public static final int FILE_INVALID=-10;
private final static String[] usernameColumnNameAsArray = {"username"};
public final static String[] getFavoritesColumnNamesAsArray = {COL_ID, COL_USERNAME};
private static final Uri FAVORITES_URI = AppDataProvider.getUriBuilderToComplete().appendPath(
AppDataProvider.FAVORITES).build();
public UserDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
this.c = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
// exception intentionally left unhandled
db.execSQL("CREATE TABLE favorites (ID TEXT PRIMARY KEY NOT NULL, username TEXT)");
if(OldDB.doesItExist(this.c)) {
upgradeFromOldDatabase(db);
}
}
private void upgradeFromOldDatabase(SQLiteDatabase newdb) {
OldDB old;
try {
old = new OldDB(this.c);
} catch(IllegalStateException e) {
// can't create database => it doesn't really exist, no matter what doesItExist() says
return;
}
int ver = old.getOldVersion();
/* version 8 was the previous version, OldDB "upgrades" itself to 1337 but unless the app
* has crashed midway through the upgrade and the user is retrying, that should never show
* up here. And if it does, try to recover favorites anyway.
* Versions < 8 already got dropped during the update process, so let's do the same.
*
* Edit: Android runs getOldVersion() then, after a while, onUpgrade(). Just to make it
* more complicated. Workaround added in OldDB.
*/
if(ver >= 8) {
ArrayList<String> ID = new ArrayList<>();
ArrayList<String> username = new ArrayList<>();
int len;
int len2;
try {
Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {});
int zero = c.getColumnIndex("busstop_ID");
int one = c.getColumnIndex("busstop_username");
while(c.moveToNext()) {
try {
ID.add(c.getString(zero));
} catch(Exception e) {
// no ID = can't add this
continue;
}
if(c.getString(one) == null || c.getString(one).length() <= 0) {
username.add(null);
} else {
username.add(c.getString(one));
}
}
c.close();
old.close();
} catch(Exception ignored) {
// there's no hope, go ahead and nuke old database.
}
len = ID.size();
len2 = username.size();
if(len2 < len) {
len = len2;
}
if (len > 0) {
try {
for (int i = 0; i < len; i++) {
final Stop mStop = new Stop(ID.get(i));
mStop.setStopUserName(username.get(i));
addOrUpdateStop(mStop, newdb);
}
} catch(Exception ignored) {
// partial data is better than no data at all, no transactions here
}
}
}
if(!OldDB.destroy(this.c)) {
// TODO: notify user somehow?
Log.e("UserDB", "Failed to delete old database, you should really uninstall and reinstall the app. Unfortunately I have no way to tell the user.");
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// nothing to do yet
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// nothing to do yet
}
/**
* Check if a stop ID is in the favorites
*
* @param db readable database
* @param stopId stop ID
* @return boolean
*/
public static boolean isStopInFavorites(SQLiteDatabase db, String stopId) {
boolean found = false;
try {
Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopId}, null, null, null);
if(c.moveToNext()) {
found = true;
}
c.close();
} catch(SQLiteException ignored) {
// don't care
}
return found;
}
/**
* Gets stop name set by the user.
*
* @param db readable database
* @param stopID stop ID
* @return name set by user, or null if not set\not found
*/
- public static String getStopUserName(SQLiteDatabase db, String stopID) {
+ public static @Nullable String getStopUserName(SQLiteDatabase db, String stopID) {
String username = null;
try {
Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null);
if(c.moveToNext()) {
int userNameIndex = c.getColumnIndex("username");
if (userNameIndex>=0)
username = c.getString(userNameIndex);
}
c.close();
- } catch(SQLiteException ignored) {}
+ } catch(SQLiteException e) {
+ Log.e("BusTO-UserDB","Cannot get stop User name for stop "+stopID+":\n"+e);
+ }
return username;
}
/**
* Get all the bus stops marked as favorites
*
* @param db
* @param dbi
* @return
*/
public static List<Stop> getFavorites(SQLiteDatabase db, StopsDBInterface dbi) {
List<Stop> l = new ArrayList<>();
Stop s;
String stopID, stopUserName;
try {
Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null);
int colID = c.getColumnIndex("ID");
int colUser = c.getColumnIndex("username");
while(c.moveToNext()) {
stopUserName = c.getString(colUser);
stopID = c.getString(colID);
s = dbi.getAllFromID(stopID);
if(s == null) {
// can't find it in database
l.add(new Stop(stopUserName, stopID, null, null, null));
} else {
// setStopName() already does sanity checks
s.setStopUserName(stopUserName);
l.add(s);
}
}
c.close();
} catch(SQLiteException ignored) {}
// comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database
Collections.sort(l);
return l;
}
public static void notifyContentProvider(Context context){
context.
getContentResolver().
notifyChange(FAVORITES_URI, null);
}
public static ArrayList<Stop> getFavoritesFromCursor(Cursor cursor, String[] columns){
List<String> colsList = Arrays.asList(columns);
if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){
throw new IllegalArgumentException();
}
ArrayList<Stop> l = new ArrayList<>();
if (cursor==null){
Log.e("UserDB-BusTO", "Null cursor given in getFavoritesFromCursor");
return l;
}
final int colID = cursor.getColumnIndex("ID");
final int colUser = cursor.getColumnIndex("username");
while(cursor.moveToNext()) {
final String stopUserName = cursor.getString(colUser);
final String stopID = cursor.getString(colID);
final Stop s = new Stop(stopID.trim());
if (stopUserName!=null) s.setStopUserName(stopUserName);
l.add(s);
}
return l;
}
public static boolean addOrUpdateStop(Stop s, SQLiteDatabase db) {
ContentValues cv = new ContentValues();
long result = -1;
String un = s.getStopUserName();
cv.put("ID", s.ID);
// is there an username?
if(un == null) {
// no: see if it's in the database
cv.put("username", getStopUserName(db, s.ID));
} else {
// yes: use it
cv.put("username", un);
}
try {
//ignore and throw -1 if the row is already in the DB
result = db.insertWithOnConflict(TABLE_NAME, null, cv,SQLiteDatabase.CONFLICT_IGNORE);
} catch (SQLiteException ignored) {}
// Android Studio suggested this unreadable replacement: return true if insert succeeded (!= -1), or try to update and return
return (result != -1) || updateStop(s, db);
}
public static boolean updateStop(Stop s, SQLiteDatabase db) {
try {
ContentValues cv = new ContentValues();
cv.put("username", s.getStopUserName());
db.update(TABLE_NAME, cv, "ID = ?", new String[]{s.ID});
return true;
} catch(SQLiteException e) {
return false;
}
}
public static boolean deleteStop(Stop s, SQLiteDatabase db) {
try {
db.delete(TABLE_NAME, "ID = ?", new String[]{s.ID});
return true;
} catch(SQLiteException e) {
return false;
}
}
public static boolean checkStopInFavorites(String stopID, Context con){
boolean found = false;
// no stop no party
if (stopID != null) {
SQLiteDatabase userDB = new UserDB(con).getReadableDatabase();
found = UserDB.isStopInFavorites(userDB, stopID);
}
return found;
}
//extract rows into CSV
public boolean writeFavoritesToCsv(CsvWriter writer){
SQLiteDatabase db = this.getReadableDatabase();
String sortOrder =
COL_ID + " DESC";
Cursor cursor = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray,null,null,null,null, sortOrder);
final int nCols = 2;//cursor.getColumnCount();
writer.writeRow(cursor.getColumnNames());
while (cursor.moveToNext()){
String[] arr = {cursor.getString(0), cursor.getString(1)};
writer.writeRow(arr);
}
cursor.close();
return true;
}
public int insertRowsFromCSV(CsvReader reader){
SQLiteDatabase db = this.getWritableDatabase();
boolean firstrow = true;
final HashMap<String,Integer> colIndexByRows = new HashMap<>();
final CloseableIterator<CsvRow> rowsIter = reader.iterator();
if (!rowsIter.hasNext()){
//nothing to do, it's an empty file
return -1;
}
final CsvRow firstRow = rowsIter.next();
// close if there isn't another rows
if(!rowsIter.hasNext()) return -2;
for (int i =0; i<firstRow.getFieldCount(); i++){
colIndexByRows.put(firstRow.getField(i),i);
}
if (!colIndexByRows.containsKey(COL_ID) || !colIndexByRows.containsKey(COL_USERNAME)){
//Cannot accept the file
return FILE_INVALID;
}
//begin
db.beginTransaction();
int updated = 0;
final int col_id = colIndexByRows.get(COL_ID);
final int col_username = colIndexByRows.get(COL_USERNAME);
while (rowsIter.hasNext()){
final CsvRow row = rowsIter.next();
final ContentValues cv = new ContentValues();
cv.put(COL_ID, row.getField(col_id));
cv.put(COL_USERNAME, row.getField(col_username));
long rowid = db.insertWithOnConflict(TABLE_NAME, null, cv, SQLiteDatabase.CONFLICT_REPLACE);
if (rowid >= 0)
updated +=1;
}
db.setTransactionSuccessful();
db.endTransaction();
db.close();
return updated;
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java
deleted file mode 100644
index f574581..0000000
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java
+++ /dev/null
@@ -1,710 +0,0 @@
-/*
- 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 <http://www.gnu.org/licenses/>.
- */
-package it.reyboz.bustorino.fragments;
-
-
-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.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.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 static it.reyboz.bustorino.fragments.ScreenBaseFragment.setOption;
-
-public class ArrivalsFragment extends ResultBaseFragment implements LoaderManager.LoaderCallbacks<Cursor> {
-
- 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<ArrivalsFetcher> fetchers = null; //new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers()));
-
- private boolean reloadOnResume = true;
-
- private final PalinaAdapter.PalinaClickListener palinaClickListener = new PalinaAdapter.PalinaClickListener() {
- @Override
- public void showRouteFullDirection(Route route) {
- String routeName;
- Log.d(DEBUG_TAG, "Make toast for line "+route.getName());
-
-
- routeName = FiveTNormalizer.routeInternalToDisplay(route.getName());
- if (routeName == null) {
- routeName = route.getDisplayCode();
- }
- 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();
- }
- }
-
- @Override
- public void requestShowingRoute(Route route) {
- Log.d(DEBUG_TAG, "Need to show line for route:\ngtfsID "+route.getGtfsId()+ " name "+route.getName());
- if(route.getGtfsId()!=null){
- mListener.showLineOnMap(route.getGtfsId(), stopID);
- } else {
- String gtfsID = FiveTNormalizer.getGtfsRouteID(route);
- Log.d(DEBUG_TAG, "GtfsID for route is: " + gtfsID);
- mListener.showLineOnMap(gtfsID, stopID);
- }
- }
- };
-
-
- 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);
- }*/
- mListener.readyGUIfor(FragmentKind.ARRIVALS);
-
- 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<Fetcher> 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, palinaClickListener, true);
- showArrivalsSources(lastUpdatedPalina);
- resetListAdapter(adapter);
-
- final ArrayList<String> routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages();
- if(routesWithNoPassages.isEmpty()){
- //hide the views if there are no empty routes
- noArrivalsRecyclerView.setVisibility(View.GONE);
- noArrivalsTitleView.setVisibility(View.GONE);
- }else{
- Collections.sort(routesWithNoPassages, new LinesNameSorter());
- noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages, null);
- if(noArrivalsRecyclerView!=null){
- noArrivalsRecyclerView.setAdapter(noArrivalsAdapter);
-
- 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.isEmpty()) {
- 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<Cursor> 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<Cursor> 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;
- if (probableName != null && !probableName.isEmpty())
- stopName = probableName; //set the stop
- //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<Cursor> 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/ArrivalsFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt
new file mode 100644
index 0000000..2791df9
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.kt
@@ -0,0 +1,796 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+ */
+package it.reyboz.bustorino.fragments
+
+import android.content.Context
+import android.database.Cursor
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.fragment.app.viewModels
+import androidx.loader.app.LoaderManager
+import androidx.loader.content.CursorLoader
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.adapters.PalinaAdapter
+import it.reyboz.bustorino.adapters.PalinaAdapter.PalinaClickListener
+import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter
+import it.reyboz.bustorino.backend.*
+import it.reyboz.bustorino.backend.DBStatusManager.OnDBUpdateStatusChangeListener
+import it.reyboz.bustorino.backend.Passaggio.Source
+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.middleware.SearchRequestType
+import it.reyboz.bustorino.util.LinesNameSorter
+import it.reyboz.bustorino.viewmodels.ArrivalsViewModel
+import java.util.*
+
+
+class ArrivalsFragment : ResultBaseFragment(), LoaderManager.LoaderCallbacks<Cursor> {
+ private var DEBUG_TAG = DEBUG_TAG_ALL
+ private lateinit var stopID: String
+ //private set
+ private var stopName: String? = null
+ private var prefs: DBStatusManager? = null
+ private var listener: OnDBUpdateStatusChangeListener? = null
+ private var justCreated = false
+ private var lastUpdatedPalina: Palina? = null
+ private var needUpdateOnAttach = false
+ private var fetchersChangeRequestPending = false
+ private var stopIsInFavorites = false
+
+ //Views
+ protected lateinit var addToFavorites: ImageButton
+ protected lateinit var timesSourceTextView: TextView
+ protected lateinit var messageTextView: TextView
+ protected lateinit var arrivalsRecyclerView: RecyclerView
+ private lateinit var mListAdapter: PalinaAdapter
+
+ private lateinit var resultsLayout : LinearLayout
+ private lateinit var loadingMessageTextView: TextView
+ private lateinit var progressBar: ProgressBar
+
+ private lateinit var howDoesItWorkTextView: TextView
+ private lateinit var hideHintButton: Button
+
+
+ //private NestedScrollView theScrollView;
+ protected lateinit var noArrivalsRecyclerView: RecyclerView
+ private var noArrivalsAdapter: RouteOnlyLineAdapter? = null
+ private var noArrivalsTitleView: TextView? = null
+ private var layoutManager: GridLayoutManager? = null
+
+ //private View canaryEndView;
+ private var fetchers: List<ArrivalsFetcher?> = ArrayList()
+ private val arrivalsViewModel : ArrivalsViewModel by viewModels()
+
+
+ private var reloadOnResume = true
+
+ fun getStopID() = stopID
+
+ private val palinaClickListener: PalinaClickListener = object : PalinaClickListener {
+ override fun showRouteFullDirection(route: Route) {
+ var routeName: String?
+ Log.d(DEBUG_TAG, "Make toast for line " + route.name)
+
+
+ routeName = FiveTNormalizer.routeInternalToDisplay(route.name)
+ if (routeName == null) {
+ routeName = route.displayCode
+ }
+ if (context == null) Log.e(DEBUG_TAG, "Touched on a route but Context is null")
+ else if (route.destinazione == null || route.destinazione.length == 0) {
+ Toast.makeText(
+ context,
+ getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ context,
+ getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ override fun requestShowingRoute(route: Route) {
+ Log.d(
+ DEBUG_TAG, """Need to show line for route: gtfsID ${route.gtfsId} name ${route.name}"""
+ )
+ if (route.gtfsId != null) {
+ mListener.showLineOnMap(route.gtfsId, stopID)
+ } else {
+ val gtfsID = FiveTNormalizer.getGtfsRouteID(route)
+ Log.d(DEBUG_TAG, "GtfsID for route is: $gtfsID")
+ mListener.showLineOnMap(gtfsID, stopID)
+ }
+ }
+ }
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ stopID = arguments!!.getString(KEY_STOP_ID) ?: ""
+ DEBUG_TAG = DEBUG_TAG_ALL + " " + stopID
+
+ //this might really be null
+ stopName = arguments!!.getString(KEY_STOP_NAME)
+ val arrivalsFragment = this
+ listener = object : OnDBUpdateStatusChangeListener {
+ override fun onDBStatusChanged(updating: Boolean) {
+ if (!updating) {
+ loaderManager.restartLoader(
+ loaderFavId,
+ arguments, arrivalsFragment
+ )
+ } else {
+ val lm = loaderManager
+ lm.destroyLoader(loaderFavId)
+ lm.destroyLoader(loaderStopId)
+ }
+ }
+
+ override fun defaultStatusValue(): Boolean {
+ return true
+ }
+ }
+ prefs = DBStatusManager(context!!.applicationContext, listener)
+ justCreated = true
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val 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)
+ //TODO: Hide this layout at the beginning, show it later
+ resultsLayout = root.findViewById(R.id.resultsLayout)
+ loadingMessageTextView = root.findViewById(R.id.loadingMessageTextView)
+ progressBar = root.findViewById(R.id.circularProgressBar)
+
+ hideHintButton.setOnClickListener(View.OnClickListener { v: View? -> this.onHideHint(v) })
+
+ //theScrollView = root.findViewById(R.id.arrivalsScrollView);
+ // recyclerview holding the arrival times
+ arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView)
+ val manager = LinearLayoutManager(context)
+ arrivalsRecyclerView.setLayoutManager(manager)
+ val mDividerItemDecoration = DividerItemDecoration(
+ arrivalsRecyclerView.context,
+ manager.orientation
+ )
+ arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration)
+ timesSourceTextView = root.findViewById(R.id.timesSourceTextView)
+ timesSourceTextView.setOnLongClickListener { view: View? ->
+ if (!fetchersChangeRequestPending) {
+ rotateFetchers()
+ //Show we are changing provider
+ timesSourceTextView.setText(R.string.arrival_source_changing)
+
+ //mListener.requestArrivalsForStopID(stopID)
+ requestArrivalsForTheFragment()
+ fetchersChangeRequestPending = true
+ return@setOnLongClickListener true
+ }
+ false
+ }
+ timesSourceTextView.setOnClickListener(View.OnClickListener { view: View? ->
+ Toast.makeText(
+ context, R.string.change_arrivals_source_message, Toast.LENGTH_SHORT
+ )
+ .show()
+ })
+ //Button
+ addToFavorites.setClickable(true)
+ addToFavorites.setOnClickListener(View.OnClickListener { v: View? ->
+ // add/remove the stop in the favorites
+ toggleLastStopToFavorites()
+ })
+
+ val displayName = arguments!!.getString(STOP_TITLE)
+ if (displayName != null) setTextViewMessage(
+ String.format(
+ getString(R.string.passages), displayName
+ )
+ )
+
+
+ val probablemessage = arguments!!.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 = GridLayoutManager(context, 60)
+ layoutManager!!.spanSizeLookup = object : SpanSizeLookup() {
+ override fun getSpanSize(position: Int): Int {
+ 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
+ lastUpdatedPalina?.let { showArrivalsSources(it) }
+ /*if (lastUpdatedPalina?.queryAllRoutes() != null && lastUpdatedPalina!!.queryAllRoutes()!!.size >0){
+ showArrivalsSources(lastUpdatedPalina!!)
+ } else{
+ Log.d(DEBUG_TAG, "No routes names")
+ }
+
+ */
+
+
+
+ arrivalsViewModel.palinaLiveData.observe(viewLifecycleOwner){
+ mListener.toggleSpinner(false)
+ if(arrivalsViewModel.resultLiveData.value==Fetcher.Result.OK){
+ //the result is true
+ changeUIFirstSearchActive(false)
+ updateFragmentData(it)
+ } else{
+ progressBar.visibility=View.INVISIBLE
+ loadingMessageTextView.text = getString(R.string.no_bus_stop_have_this_name)
+ }
+
+ }
+
+ arrivalsViewModel.sourcesLiveData.observe(viewLifecycleOwner){
+ Log.d(DEBUG_TAG, "Using arrivals source: $it")
+ val srcString = getDisplayArrivalsSource(it,requireContext())
+ loadingMessageTextView.text = getString(R.string.searching_arrivals_fmt, srcString)
+ }
+
+ arrivalsViewModel.resultLiveData.observe(viewLifecycleOwner){res ->
+ when (res) {
+ Fetcher.Result.OK -> {}
+ Fetcher.Result.CLIENT_OFFLINE -> showToastMessage(R.string.network_error, true)
+ Fetcher.Result.SERVER_ERROR -> {
+ if (utils.isConnected(context)) {
+ showToastMessage(R.string.parsing_error, true)
+ } else {
+ showToastMessage(R.string.network_error, true)
+ }
+ showToastMessage(R.string.internal_error,true)
+ }
+
+ Fetcher.Result.PARSER_ERROR -> showShortToast(R.string.internal_error)
+ Fetcher.Result.QUERY_TOO_SHORT -> showShortToast(R.string.query_too_short)
+ Fetcher.Result.EMPTY_RESULT_SET -> showShortToast(R.string.no_arrivals_stop)
+
+ Fetcher.Result.NOT_FOUND -> showShortToast(R.string.no_bus_stop_have_this_name)
+ else -> showShortToast(R.string.internal_error)
+ }
+ }
+ return root
+ }
+
+
+ private fun showShortToast(id: Int) = showToastMessage(id,true)
+
+
+ private fun changeUIFirstSearchActive(yes: Boolean){
+ if(yes){
+ resultsLayout.visibility = View.GONE
+ progressBar.visibility = View.VISIBLE
+ loadingMessageTextView.visibility = View.VISIBLE
+ } else{
+ resultsLayout.visibility = View.VISIBLE
+ progressBar.visibility = View.GONE
+ loadingMessageTextView.visibility = View.GONE
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val loaderManager = loaderManager
+ Log.d(DEBUG_TAG, "OnResume, justCreated $justCreated, lastUpdatedPalina is: $lastUpdatedPalina")
+ /*if(needUpdateOnAttach){
+ updateFragmentData(null);
+ needUpdateOnAttach=false;
+ }*/
+ /*if(lastUpdatedPalina!=null){
+ updateFragmentData(null);
+ showArrivalsSources(lastUpdatedPalina);
+ }*/
+ mListener.readyGUIfor(FragmentKind.ARRIVALS)
+
+ resetListAdapter(mListAdapter)
+ if (noArrivalsAdapter != null) {
+ noArrivalsRecyclerView.adapter = noArrivalsAdapter
+ }
+
+ if (stopID.isNotEmpty()) {
+ if (!justCreated) {
+ fetchers = utils.getDefaultArrivalsFetchers(context)
+ adjustFetchersToSource()
+
+ if (reloadOnResume) requestArrivalsForTheFragment() //mListener.requestArrivalsForStopID(stopID)
+ } else {
+ //start first search
+ requestArrivalsForTheFragment()
+ changeUIFirstSearchActive(true)
+ justCreated = false
+ }
+ //start the loader
+ if (prefs!!.isDBUpdating(true)) {
+ prefs!!.registerListener()
+ } else {
+ Log.d(DEBUG_TAG, "Restarting loader for stop")
+ loaderManager.restartLoader(
+ loaderFavId,
+ arguments, this
+ )
+ }
+ updateMessage()
+ }
+
+ if (ScreenBaseFragment.getOption(requireContext(), OPTION_SHOW_LEGEND, true)) {
+ showHints()
+ }
+ }
+
+
+ override fun onStart() {
+ super.onStart()
+ if (needUpdateOnAttach) {
+ updateFragmentData(null)
+ needUpdateOnAttach = false
+ }
+ }
+
+ override fun onPause() {
+ if (listener != null) prefs!!.unregisterListener()
+ super.onPause()
+ val loaderManager = loaderManager
+ Log.d(DEBUG_TAG, "onPause, have running loaders: " + loaderManager.hasRunningLoaders())
+ loaderManager.destroyLoader(loaderFavId)
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ //get fetchers
+ fetchers = utils.getDefaultArrivalsFetchers(context)
+ }
+
+ fun reloadsOnResume(): Boolean {
+ return reloadOnResume
+ }
+
+ fun setReloadOnResume(reloadOnResume: Boolean) {
+ this.reloadOnResume = reloadOnResume
+ }
+
+ // HINT "HOW TO USE"
+ private fun showHints() {
+ howDoesItWorkTextView!!.visibility = View.VISIBLE
+ hideHintButton!!.visibility = View.VISIBLE
+ //actionHelpMenuItem.setVisible(false);
+ }
+
+ private fun hideHints() {
+ howDoesItWorkTextView!!.visibility = View.GONE
+ hideHintButton!!.visibility = View.GONE
+ //actionHelpMenuItem.setVisible(true);
+ }
+
+ fun onHideHint(v: View?) {
+ hideHints()
+ ScreenBaseFragment.setOption(requireContext(), OPTION_SHOW_LEGEND, false)
+ }
+
+ val currentFetchers: ArrayList<Fetcher?>
+ /**
+ * Give the fetchers
+ * @return the list of the fetchers
+ */
+ get() = ArrayList(this.fetchers)
+ /*val currentFetchersAsArray: Array<ArrivalsFetcher?>
+ get() {
+ val arr = arrayOfNulls<ArrivalsFetcher>(fetchers!!.size)
+ fetchers!!.toArray<ArrivalsFetcher>(arr)
+ return arr
+ }
+
+ */
+
+ fun getCurrentFetchersAsArray(): Array<out ArrivalsFetcher?> {
+ val r= fetchers.toTypedArray() ?: emptyArray<ArrivalsFetcher>()
+ //?: emptyArray<ArrivalsFetcher>()
+ return r
+ }
+
+ private fun rotateFetchers() {
+ Log.d(DEBUG_TAG, "Rotating fetchers, before: $fetchers")
+ fetchers?.let { Collections.rotate(it, -1) }
+ Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: $fetchers")
+ }
+
+
+ /**
+ * Update the UI with the new data
+ * @param p the full Palina
+ */
+ fun updateFragmentData(p: Palina?) {
+ 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 {
+ val adapter = PalinaAdapter(context, lastUpdatedPalina, palinaClickListener, true)
+ showArrivalsSources(lastUpdatedPalina!!)
+ resetListAdapter(adapter)
+
+ val routesWithNoPassages = lastUpdatedPalina!!.routesNamesWithNoPassages
+ if (routesWithNoPassages.isEmpty()) {
+ //hide the views if there are no empty routes
+ noArrivalsRecyclerView!!.visibility = View.GONE
+ noArrivalsTitleView!!.visibility = View.GONE
+ } else {
+ Collections.sort(routesWithNoPassages, LinesNameSorter())
+ noArrivalsAdapter = RouteOnlyLineAdapter(routesWithNoPassages, null)
+ if (noArrivalsRecyclerView != null) {
+ noArrivalsRecyclerView!!.adapter = noArrivalsAdapter
+
+ noArrivalsRecyclerView!!.visibility = View.VISIBLE
+ noArrivalsTitleView!!.visibility = 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 fun showArrivalsSources(p: Palina) {
+ val source = p.passaggiSourceIfAny
+ val source_txt = getDisplayArrivalsSource(source, requireContext())
+ //
+ val updatedFetchers = adjustFetchersToSource(source)
+ if (!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work")
+ val base_message = getString(R.string.times_source_fmt, source_txt)
+ timesSourceTextView!!.text = base_message
+ timesSourceTextView!!.visibility = View.VISIBLE
+
+ if (p.totalNumberOfPassages > 0) {
+ timesSourceTextView!!.visibility = View.VISIBLE
+ } else {
+ timesSourceTextView!!.visibility = View.INVISIBLE
+ }
+ fetchersChangeRequestPending = false
+ }
+
+ protected fun adjustFetchersToSource(source: Passaggio.Source?): Boolean {
+ if (source == null) return false
+ var count = 0
+ if (source != Passaggio.Source.UNDETERMINED) while (source != fetchers!![0]!!.sourceForFetcher && count < 200) {
+ //we need to update the fetcher that is requested
+ rotateFetchers()
+ count++
+ }
+ return count < 200
+ }
+
+ protected fun adjustFetchersToSource(): Boolean {
+ if (lastUpdatedPalina == null) return false
+ val source = lastUpdatedPalina!!.passaggiSourceIfAny
+ return adjustFetchersToSource(source)
+ }
+
+ /**
+ * Update the message in the fragment
+ *
+ * It may eventually change the "Add to Favorite" icon
+ */
+ private fun updateMessage() {
+ var message: String? = null
+ if (stopName != null && stopID != null && !stopName!!.isEmpty()) {
+ message = ("$stopID - $stopName")
+ } else if (stopID != null) {
+ message = stopID
+ } else {
+ Log.e("ArrivalsFragm$tag", "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();
+ }
+
+ override fun onCreateLoader(id: Int, p1: Bundle?): Loader<Cursor> {
+ val args = arguments
+ //if (args?.getString(KEY_STOP_ID) == null) throw
+ val stopID = args?.getString(KEY_STOP_ID) ?: ""
+ val builder = AppDataProvider.getUriBuilderToComplete()
+ val cl: CursorLoader
+ when (id) {
+ loaderFavId -> {
+ builder.appendPath("favorites").appendPath(stopID)
+ cl = CursorLoader(context!!, builder.build(), UserDB.getFavoritesColumnNamesAsArray, null, null, null)
+ }
+
+ loaderStopId -> {
+ builder.appendPath("stop").appendPath(stopID)
+ cl = CursorLoader(
+ context!!, builder.build(), arrayOf(NextGenDB.Contract.StopsTable.COL_NAME),
+ null, null, null
+ )
+ }
+
+ else -> {
+ cl = CursorLoader(context!!, builder.build(), null, null,null,null)
+ Log.d(DEBUG_TAG, "This is probably going to crash")
+ }
+ }
+ cl.setUpdateThrottle(500)
+ return cl
+ }
+
+ override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor) {
+ when (loader.id) {
+ loaderFavId -> {
+ val colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1])
+ if (data.count > 0) {
+ // IT'S IN FAVORITES
+ data.moveToFirst()
+ val probableName = data.getString(colUserName)
+ stopIsInFavorites = true
+ if (probableName != null && !probableName.isEmpty()) stopName = probableName //set the stop
+
+ //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$tag", "Stop wasn't in the favorites and has no name, looking in the DB")
+ loaderManager.restartLoader(
+ loaderStopId,
+ arguments, this
+ )
+ }
+ }
+
+ loaderStopId -> if (data.count > 0) {
+ data.moveToFirst()
+ val 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$tag", "Stop is not inside the database... CLOISTER BELL")
+ }
+ }
+ }
+
+ override fun onLoaderReset(loader: Loader<Cursor>) {
+ //NOTHING TO DO
+ }
+
+ protected fun resetListAdapter(adapter: PalinaAdapter) {
+ mListAdapter = adapter
+ arrivalsRecyclerView.adapter = adapter
+ arrivalsRecyclerView.visibility = View.VISIBLE
+ }
+
+ /**
+ * Set the message textView
+ * @param message the whole message to write in the textView
+ */
+ fun setTextViewMessage(message: String?) {
+ messageTextView!!.text = message
+ messageTextView!!.visibility = View.VISIBLE
+ }
+
+ fun toggleLastStopToFavorites() {
+ val stop: Stop? = lastUpdatedPalina
+ if (stop != null) {
+ // toggle the status in background
+
+ AsyncStopFavoriteAction(
+ context!!.applicationContext, AsyncStopFavoriteAction.Action.TOGGLE
+ ) { v: Boolean -> 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
+ */
+ fun updateStarIconFromLastBusStop(toggleDone: Boolean) {
+ stopIsInFavorites = if (stopIsInFavorites) !toggleDone
+ else 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`
+ */
+ fun updateStarIcon() {
+ // no favorites no party!
+
+ // check if there is a last Stop
+
+ if (stopID == null) {
+ addToFavorites!!.visibility = View.INVISIBLE
+ } else {
+ // filled or outline?
+ if (stopIsInFavorites) {
+ addToFavorites!!.setImageResource(R.drawable.ic_star_filled)
+ } else {
+ addToFavorites!!.setImageResource(R.drawable.ic_star_outline)
+ }
+
+ addToFavorites!!.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onDestroyView() {
+ //arrivalsRecyclerView = null
+ if (arguments != null) {
+ arguments!!.putString(SOURCES_TEXT, timesSourceTextView!!.text.toString())
+ arguments!!.putString(MESSAGE_TEXT_VIEW, messageTextView!!.text.toString())
+ }
+ super.onDestroyView()
+ }
+
+ override fun getBaseViewForSnackBar(): View? {
+ return null
+ }
+
+ fun isFragmentForTheSameStop(p: Palina): Boolean {
+ return if (tag != null) tag == getFragmentTag(p)
+ else false
+ }
+
+
+ /**
+ * Request arrivals in the fragment
+ */
+ fun requestArrivalsForTheFragment(){
+
+ // Run with previous fetchers
+ //fragment.getCurrentFetchers().toArray()
+ //AsyncArrivalsSearcher(, getCurrentFetchersAsArray(), context).execute(stopID)
+ context?.let {
+ mListener.toggleSpinner(true)
+ val fetcherSources = fetchers.map { f-> f?.sourceForFetcher?.name ?: "" }
+ //val workRequest = ArrivalsWorker.buildWorkRequest(stopID, fetcherSources.toTypedArray())
+ //val workManager = WorkManager.getInstance(it)
+
+ //workManager.enqueueUniqueWork(getArrivalsWorkID(stopID), ExistingWorkPolicy.REPLACE, workRequest)
+
+ arrivalsViewModel.requestArrivalsForStop(stopID,fetcherSources.toTypedArray())
+
+ //prepareGUIForArrivals();
+ //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID);
+ Log.d(DEBUG_TAG, "Started search for arrivals of stop $stopID")
+ }
+ }
+
+ companion object {
+ private const val OPTION_SHOW_LEGEND = "show_legend"
+ private const val KEY_STOP_ID = "stopid"
+ private const val KEY_STOP_NAME = "stopname"
+ private const val DEBUG_TAG_ALL = "BUSTOArrivalsFragment"
+ private const val loaderFavId = 2
+ private const val loaderStopId = 1
+ const val STOP_TITLE: String = "messageExtra"
+ private const val SOURCES_TEXT = "sources_textview_message"
+
+ @JvmStatic
+ @JvmOverloads
+ fun newInstance(stopID: String, stopName: String? = null): ArrivalsFragment {
+ val fragment = ArrivalsFragment()
+ val args = 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.arguments = args
+ return fragment
+ }
+
+ @JvmStatic
+ fun getFragmentTag(p: Palina): String {
+ return "palina_" + p.ID
+ }
+
+ @JvmStatic
+ fun getArrivalsWorkID(stopID: String) = "arrivals_search_$stopID"
+
+ @JvmStatic
+ fun getDisplayArrivalsSource(source: Source, context: Context): String{
+ return when (source) {
+ Passaggio.Source.GTTJSON -> context.getString(R.string.gttjsonfetcher)
+ Passaggio.Source.FiveTAPI -> context.getString(R.string.fivetapifetcher)
+ Passaggio.Source.FiveTScraper -> context.getString(R.string.fivetscraper)
+ Passaggio.Source.MatoAPI -> context.getString(R.string.source_mato)
+ Passaggio.Source.UNDETERMINED -> //Don't show the view
+ context.getString(R.string.undetermined_source)
+
+ }
+ }
+ }
+}
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 20d9817..696199f 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/MainScreenFragment.java
@@ -1,881 +1,884 @@
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.EditText;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
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.data.PreferencesHolder;
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 org.jetbrains.annotations.NotNull;
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 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 MenuItem actionHelpMenuItem;
private FloatingActionButton floatingActionButton;
private FrameLayout resultFrameLayout;
private boolean setupOnStart = true;
private boolean suppressArrivalsReload = false;
//private Snackbar snackbar;
/*
* Search mode
*/
private static final int SEARCH_BY_NAME = 0;
private static final int SEARCH_BY_ID = 1;
//private static final int SEARCH_BY_ROUTE = 2; // implement this -- DONE!
private int searchMode;
//private ImageButton addToFavorites;
//// HIDDEN BUT IMPORTANT ELEMENTS ////
FragmentManager childFragMan;
Handler mainHandler;
private final Runnable refreshStop = new Runnable() {
public void run() {
if(getContext() == null) return;
List<ArrivalsFetcher> fetcherList = utils.getDefaultArrivalsFetchers(getContext());
ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[fetcherList.size()];
arrivalsFetchers = fetcherList.toArray(arrivalsFetchers);
if (childFragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) childFragMan.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();
+ //String stopName = fragment.getStopID();
- new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName);
+ //new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName);
+ fragment.requestArrivalsForTheFragment();
}
} else //we create a new fragment, which is WRONG
new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute();
}
};
//
private final ActivityResultLauncher<BarcodeScanOptions> 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 pendingIntroRun = false;
boolean pendingNearbyStopsFragmentRequest = false;
boolean locationPermissionGranted, locationPermissionAsked = false;
AppLocationManager locationManager;
private final ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
if(result==null) return;
if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||
result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null)
return;
Log.d(DEBUG_TAG, "Permissions for location are: "+result);
if(Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION))
|| Boolean.TRUE.equals(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(pendingNearbyStopsFragmentRequest){
showNearbyFragmentIfPossible();
pendingNearbyStopsFragmentRequest = false;
}
}
if(pendingNearbyStopsFragmentRequest) pendingNearbyStopsFragmentRequest =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));
showNearbyFragmentIfPossible();
}
}
@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");
pendingNearbyStopsFragmentRequest = true;
//mainHandler.post(new NearbyStopsRequester(getContext(), cr));
showNearbyFragmentIfPossible();
}
}
@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);
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));
AppCompatImageButton qrButton = root.findViewById(R.id.QRButton);
qrButton.setOnClickListener(this::onQRButtonClick);
AppCompatImageButton searchButton = root.findViewById(R.id.searchButton);
searchButton.setOnClickListener(this::onSearchClick);
// Fragment stuff
childFragMan = getChildFragmentManager();
childFragMan.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);
}
}
@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);
}
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){
if(PreferencesHolder.hasIntroFinishedOneShot(requireContext())){
Log.d(DEBUG_TAG, "Showing nearby stops");
if(!checkLocationPermission()){
requestLocationPermission();
pendingNearbyStopsFragmentRequest = true;
}
else {
showNearbyFragmentIfPossible();
}
} else {
//The Introductory Activity is about to be started, hence pause the request and show later
pendingIntroRun = true;
}
}
else{
///TODO: if there is a stop displayed, we need to hold the update
}
setupOnStart = false;
}
}
@Override
public void onResume() {
super.onResume();
final Context con = requireContext();
Log.w(DEBUG_TAG, "OnResume called, setupOnStart: "+ setupOnStart);
if (locationManager == null)
locationManager = AppLocationManager.getInstance(con);
//recheck the introduction activity has been run
if(pendingIntroRun && PreferencesHolder.hasIntroFinishedOneShot(con)){
//request position permission if needed
if(!checkLocationPermission()){
requestLocationPermission();
pendingNearbyStopsFragmentRequest = true;
}
else {
showNearbyFragmentIfPossible();
}
//deactivate flag
pendingIntroRun = false;
}
if(Permissions.bothLocationPermissionsGranted(con)){
Log.d(DEBUG_TAG, "Location permission OK");
if(!locationManager.isRequesterRegistered(requester))
locationManager.addLocationRequestFor(requester);
} //don't request permission
// 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);
//TODO: if we come back to this from another fragment, and the user has given again the permission
// for the Location, we should show the Nearby Stops
if(!suppressArrivalsReload && pendingStopID==null){
//none of the following cases are true
// check if we are showing any fragment
final Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame);
if(fragment==null || swipeRefreshLayout.getVisibility() != View.VISIBLE){
//we are not showing anything
if(Permissions.anyLocationPermissionsGranted(getContext())){
showNearbyFragmentIfPossible();
}
}
}
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);
}
}
/**
* 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.isResumed());
}
/**
* 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());
}
@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() {
+ private void prepareGUIForArrivals() {
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 = childFragMan.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 = childFragMan.beginTransaction();
ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG);
if (getActivity()!=null && !getActivity().isFinishing())
ft.commit();
else Log.e(DEBUG_TAG, "Not showing nearby fragment because activity null or is finishing");
}
}
@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 (pendingNearbyStopsFragmentRequest) {
locationManager.removeLocationRequestFor(requester);
pendingNearbyStopsFragmentRequest = false;
}
}
if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType");
else
switch (fragmentType) {
case ARRIVALS:
- prepareGUIForBusLines();
+ prepareGUIForArrivals();
break;
case STOPS:
prepareGUIForBusStops();
break;
default:
Log.d(DEBUG_TAG, "Fragment type is unknown");
return;
}
// Shows hints
}
@Override
public void showLineOnMap(String routeGtfsId, @Nullable String stopIDFrom) {
//pass to activity
mListener.showLineOnMap(routeGtfsId, stopIDFrom);
}
@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);
+ fragment.requestArrivalsForTheFragment();
} else{
- new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID);
+ //SHOW NEW ARRIVALS FRAGMENT
+ //new AsyncArrivalsSearcher(fragmentHelper, fetchers, getContext()).execute(ID);
+ fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true);
}
}
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);
+ //prepareGUIForArrivals();
+ //new AsyncArrivalsSearcher(fragmentHelper,fetchers, getContext()).execute(ID);
+ fragmentHelper.createOrUpdateStopFragment(new Palina(ID), true);
+
}
}
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 showNearbyFragmentIfPossible() {
if (isNearbyFragmentShown()) {
//nothing to do
Log.w(DEBUG_TAG, "Asked to show nearby fragment but we already are showing it");
return;
}
if (getContext() == null) {
Log.e(DEBUG_TAG, "Wanting to show nearby fragment but context is null");
return;
}
if (fragmentHelper.getLastSuccessfullySearchedBusStop() == null
&& !childFragMan.isDestroyed()) {
//Go ahead with the request
actuallyShowNearbyStopsFragment();
pendingNearbyStopsFragmentRequest = false;
}
}
/////////// 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/ResultBaseFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java
index a36a550..1a77b6c 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultBaseFragment.java
@@ -1,33 +1,33 @@
package it.reyboz.bustorino.fragments;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
-public abstract class ResultBaseFragment extends Fragment {
+public abstract class ResultBaseFragment extends ScreenBaseFragment {
protected FragmentListenerMain mListener;
protected static final String MESSAGE_TEXT_VIEW = "message_text_view";
public ResultBaseFragment() {
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof FragmentListenerMain) {
mListener = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement FragmentListenerMain");
}
}
@Override
public void onDetach() {
mListener.showFloatingActionButton(false);
mListener = null;
super.onDetach();
}
}
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 70b262c..bb14687 100644
--- a/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
+++ b/app/src/main/java/it/reyboz/bustorino/fragments/ScreenBaseFragment.java
@@ -1,104 +1,107 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import it.reyboz.bustorino.BuildConfig;
import java.util.Map;
import static android.content.Context.MODE_PRIVATE;
public abstract class ScreenBaseFragment extends Fragment {
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();
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();
+ final Context context = getContext();
+ if(context!=null)
+ Toast.makeText(context, 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 a view or null if you don't want the snackbar shown
*/
@Nullable
public abstract View getBaseViewForSnackBar();
/**
* Empty method to override properties of the Snackbar before showing it
* @param snackbar the Snackbar to be possibly modified
*/
public void setSnackbarPropertiesBeforeShowing(Snackbar snackbar){
}
public boolean showSnackbarOnDBUpdate() {
return true;
}
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();
}
public ActivityResultLauncher<String[]> getPositionRequestLauncher(LocationRequestListener listener){
return registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
if (result == null) return;
if (result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||
result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null)
return;
final boolean coarseGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_COARSE_LOCATION));
final boolean fineGranted = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION));
listener.onPermissionResult(coarseGranted, fineGranted);
}
});
}
public interface LocationRequestListener{
void onPermissionResult(boolean isCoarseGranted, boolean isFineGranted);
}
+
}
diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
new file mode 100644
index 0000000..f62e93b
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/ArrivalsViewModel.kt
@@ -0,0 +1,184 @@
+package it.reyboz.bustorino.viewmodels
+
+import android.app.Application
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.viewModelScope
+import it.reyboz.bustorino.backend.*
+import it.reyboz.bustorino.backend.mato.MatoAPIFetcher
+import it.reyboz.bustorino.data.NextGenDB
+import it.reyboz.bustorino.middleware.RecursionHelper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.util.concurrent.atomic.AtomicReference
+
+class ArrivalsViewModel(application: Application): AndroidViewModel(application) {
+
+ // Arrivals of palina
+ val appContext: Context
+ init {
+ appContext = application.applicationContext
+ }
+
+ val palinaLiveData = MediatorLiveData<Palina>()
+ val sourcesLiveData = MediatorLiveData<Passaggio.Source>()
+
+ val resultLiveData = MediatorLiveData<Fetcher.Result>()
+
+ val currentFetchers = MediatorLiveData<List<ArrivalsFetcher>>()
+
+ fun requestArrivalsForStop(stopId: String, fetchers: List<ArrivalsFetcher>){
+ val context = appContext //application.applicationContext
+ currentFetchers.value = fetchers
+ viewModelScope.launch(Dispatchers.IO){
+ runArrivalsFetching(stopId, fetchers, context)
+ }
+ }
+
+ fun requestArrivalsForStop(stopId: String, fetchersSources: Array<String>){
+ val fetchers = constructFetchersFromStrList(fetchersSources)
+ requestArrivalsForStop(stopId, fetchers)
+ }
+
+ private suspend fun runArrivalsFetching(stopId: String, fetchers: List<ArrivalsFetcher>, appContext: Context) {
+
+ if (fetchers.isEmpty()) {
+ //do nothing
+ return
+ }
+
+ // Equivalente del doInBackground nell'AsyncTask
+ val recursionHelper = RecursionHelper(fetchers.toTypedArray())
+ var resultPalina = Palina(stopId)
+
+ val stringBuilder = StringBuilder()
+ for (f in fetchers) {
+ stringBuilder.append("")
+ stringBuilder.append(f.javaClass.simpleName)
+ stringBuilder.append("; ")
+ }
+ Log.d(DEBUG_TAG, "Using fetchers: $stringBuilder")
+
+ val resultRef = AtomicReference<Fetcher.Result>()
+
+ while (recursionHelper.valid()) {
+
+ val fetcher = recursionHelper.getAndMoveForward()
+
+ sourcesLiveData.postValue(fetcher.sourceForFetcher)
+
+
+ if (fetcher is MatoAPIFetcher) {
+ fetcher.appContext = appContext
+ }
+ Log.d(DEBUG_TAG, "Using the ArrivalsFetcher: ${fetcher.javaClass}")
+
+ // Verifica se è un fetcher per MetroStop da saltare
+ try {
+ if (fetcher is FiveTAPIFetcher && stopId.toInt() >= 8200) {
+ continue
+ }
+ } catch (ex: NumberFormatException) {
+ Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures")
+ }
+
+ // Legge i tempi di arrivo
+ val palina = fetcher.ReadArrivalTimesAll(stopId, resultRef)
+
+ Log.d(DEBUG_TAG, "Arrivals fetcher: $fetcher\n\tProgress: ${resultRef.get()}")
+
+
+ // Gestione del FiveTAPIFetcher per ottenere le direzioni
+ if (fetcher is FiveTAPIFetcher) {
+ val branchResultRef = AtomicReference<Fetcher.Result>()
+ val branches = fetcher.getDirectionsForStop(stopId, branchResultRef)
+ Log.d(DEBUG_TAG, "FiveTArrivals fetcher: $fetcher\n\tDetails req: ${branchResultRef.get()}")
+
+ if (branchResultRef.get() == Fetcher.Result.OK) {
+ palina.addInfoFromRoutes(branches)
+
+ // Inserisce i dati nel database
+ viewModelScope.launch(Dispatchers.IO) {
+ //modify the DB in another coroutine in the background
+ NextGenDB.insertBranchesIntoDB(appContext,branches)
+ }
+
+ } else {
+ resultRef.set(Fetcher.Result.NOT_FOUND)
+ }
+ }
+
+ // Unisce percorsi duplicati
+ palina.mergeDuplicateRoutes(0)
+
+ if (resultRef.get() == Fetcher.Result.OK && palina.getTotalNumberOfPassages() == 0) {
+ resultRef.set(Fetcher.Result.EMPTY_RESULT_SET)
+ Log.d(DEBUG_TAG, "Setting empty results")
+ }
+ //reportProgress
+ resultLiveData.postValue(resultRef.get())
+
+ // Se è un MatoAPIFetcher con risultati validi, salviamo i dati
+ if (resultPalina == null && fetcher is MatoAPIFetcher && palina.queryAllRoutes().size > 0) {
+ resultPalina = palina
+ }
+
+ // Se abbiamo un risultato OK, restituiamo la palina
+ if (resultRef.get() == Fetcher.Result.OK) {
+ //set data
+ resultLiveData.postValue(Fetcher.Result.OK)
+ palinaLiveData.postValue(palina)
+ //TODO: Rotate the fetchers appropriately
+ return
+ }
+ //end Fetchers loop
+ }
+
+ // Se arriviamo qui, tutti i fetcher hanno fallito
+ //failedAll = true
+
+ // Se abbiamo comunque una palina, la restituiamo
+ if (resultPalina != null) {
+ resultLiveData.postValue(resultRef.get())
+ palinaLiveData.postValue(resultPalina)
+ }
+
+ }
+
+ companion object{
+ const val DEBUG_TAG="BusTO-ArrivalsViMo"
+
+ @JvmStatic
+ fun getFetcherFromStrSource(src:String): ArrivalsFetcher?{
+ val srcEnum = Passaggio.Source.valueOf(src)
+
+ val fe: ArrivalsFetcher? = when(srcEnum){
+ Passaggio.Source.FiveTAPI -> FiveTAPIFetcher()
+ Passaggio.Source.GTTJSON -> GTTJSONFetcher()
+ Passaggio.Source.FiveTScraper -> FiveTScraperFetcher()
+ Passaggio.Source.MatoAPI -> MatoAPIFetcher()
+ Passaggio.Source.UNDETERMINED -> null
+ null -> null
+ }
+ return fe
+ }
+
+ @JvmStatic
+ fun constructFetchersFromStrList(sources: Array<String>): List<ArrivalsFetcher>{
+ val fetchers = mutableListOf<ArrivalsFetcher>()
+ for(s in sources){
+ val fe = getFetcherFromStrSource(s)
+ if(fe!=null){
+ fetchers.add(fe)
+ } else{
+ Log.d(DEBUG_TAG, "Cannot convert fetcher source $s to a fetcher")
+ }
+ }
+
+ return fetchers
+ }
+ }
+
+}
\ 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 03440e8..e00037c 100644
--- a/app/src/main/res/layout/fragment_arrivals.xml
+++ b/app/src/main/res/layout/fragment_arrivals.xml
@@ -1,181 +1,204 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="8dp"
android:animateLayoutChanges="true">
<androidx.cardview.widget.CardView
android:id="@+id/messageCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="5dp"
app:cardElevation="2dp"
android:layout_alignParentTop="true"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
>
<TextView
android:id="@+id/messageTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_toStartOf="@+id/addToFavorites"
android:layout_toLeftOf="@+id/addToFavorites"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="40dp"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<ImageButton
android:id="@+id/addToFavorites"
android:layout_width="45dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="@android:color/transparent"
android:foreground="?attr/selectableItemBackground"
app:srcCompat="@drawable/ic_star_outline"
tools:ignore="OnClick"/>
</androidx.cardview.widget.CardView>
+ <ProgressBar
+ style="?android:attr/progressBarStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/circularProgressBar"
+ android:layout_marginTop="25dp"
+ android:progressDrawable="?colorPrimary"
+ android:indeterminate="true"
+ android:indeterminateTint="?colorAccent"
+ android:layout_below="@id/messageCardView"
+
+ android:layout_centerHorizontal="true"/>
+ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:id="@+id/loadingMessageTextView"
+ android:layout_below="@id/circularProgressBar"
+ android:text=""
+ android:layout_marginTop="8dp"
+ android:textSize="15sp"
+ android:layout_centerHorizontal="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ />
+
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/messageCardView"
+ android:id="@+id/resultsLayout"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/arrivalsScrollView"
android:layout_weight="12"
>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent" android:layout_height="wrap_content">
<TextView
android:id="@+id/howDoesItWorkTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dip"
android:layout_marginTop="10dp"
android:text="@string/howDoesItWork"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/blue_500"
android:visibility="gone"
/>
<Button
android:id="@+id/hideHintButton"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:minWidth="80dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:layout_marginBottom="10dp"
android:background="@drawable/route_background_bus"
android:text="@string/hint_button"
android:textColor="@color/grey_100"
android:textSize="19sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/arrivalsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:descendantFocusability="blocksDescendants"
android:focusable="true"
android:visibility="visible"
android:nestedScrollingEnabled="false"
android:layout_marginTop="8dp"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_passages_title"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
android:textSize="22sp"
android:id="@+id/noArrivalsMessageTextView"
android:minHeight="0dp"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:visibility="gone"
/>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/noArrivalsRecyclerView"
android:nestedScrollingEnabled="false"
android:layout_marginLeft="@dimen/margin_arr"
android:layout_marginStart="@dimen/margin_arr"
android:layout_marginEnd="@dimen/margin_arr"
android:layout_marginRight="@dimen/margin_arr"
/>
<!--
<View
android:id="@+id/canaryEndView"
android:layout_height="2dp"
android:layout_width="match_parent"
android:layout_marginLeft="@dimen/margin_arr"
android:layout_marginStart="@dimen/margin_arr"
android:layout_marginEnd="@dimen/margin_arr"
android:layout_marginRight="@dimen/margin_arr"
android:background="@color/orange_500"
/>
-->
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/bottomLinearLayout"
android:gravity="top"
>
<View
android:id="@+id/divider_arr"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/grey_200"
/>
<TextView
android:id="@+id/timesSourceTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="20sp"
android:gravity="center_vertical"
android:paddingBottom="5dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main_screen.xml b/app/src/main/res/layout/fragment_main_screen.xml
index 4d1d50c..26fdb0b 100644
--- a/app/src/main/res/layout/fragment_main_screen.xml
+++ b/app/src/main/res/layout/fragment_main_screen.xml
@@ -1,147 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.MainScreenFragment"
android:paddingTop="10dip"
>
<ImageButton
android:id="@+id/QRButton"
style="?android:attr/borderlessButtonStyle"
android:layout_width="40dip"
android:layout_height="40dip"
android:layout_alignBottom="@+id/searchButton"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:background="@drawable/qrcode_button_custom"
android:contentDescription="@string/qrcode"
/>
<EditText
android:id="@+id/busStopSearchByIDEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:layout_toEndOf="@+id/QRButton"
android:layout_toLeftOf="@+id/searchButton"
android:layout_toRightOf="@+id/QRButton"
android:layout_toStartOf="@+id/searchButton"
android:ems="10"
android:hint="@string/insert_bus_stop_number"
android:imeOptions="actionSearch"
android:inputType="number"
android:selectAllOnFocus="true"
android:singleLine="true"
>
<requestFocus />
</EditText>
<!--android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"-->
<EditText
android:id="@+id/busStopSearchByNameEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:layout_toEndOf="@+id/QRButton"
android:layout_toLeftOf="@+id/searchButton"
android:layout_toRightOf="@+id/QRButton"
android:layout_toStartOf="@+id/searchButton"
android:inputType="text"
android:ems="10"
android:hint="@string/insert_bus_stop_name"
android:imeOptions="actionSearch"
android:singleLine="true"
android:visibility="gone">
<requestFocus />
</EditText>
<ImageButton
android:id="@+id/searchButton"
style="?android:attr/borderlessButtonStyle"
android:layout_width="40dip"
android:layout_height="40dip"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/busStopSearchByIDEditText"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:background="@drawable/search_button_custom"
android:contentDescription="@string/search"
/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_height="8dp"
android:layout_alignParentEnd="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/searchButton"
- android:layout_marginEnd="16dp"
- android:layout_marginLeft="16dip"
- android:layout_marginRight="16dp"
- android:layout_marginStart="16dip"
+ android:layout_marginEnd="10dp"
+ android:layout_marginStart="10dp"
android:indeterminateOnly="true"
android:minWidth="10dp"
android:visibility="gone" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/coord_layout"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_gravity="bottom|end"
android:layout_below="@+id/searchButton"
>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/listRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:animateLayoutChanges="true"
android:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:id="@+id/resultFrame"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_gravity="bottom|end"
android:layout_margin="13dp"
android:src="@drawable/alphabetical"
fabSize="normal"
backgroundTint="@color/teal_500"
rippleColor="@color/teal_300"
elevation="13dp"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 8195d32..0085dfc 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1,267 +1,269 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_description">Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy.</string>
<string name="search">Cerca</string>
<string name="qrcode">Codice QR</string>
<string name="yes">Si</string>
<string name="no">No</string>
<string name="next">Prossimo</string>
<string name="previous">Precedente</string>
<string name="title_barcode_scanner_install">Installare Barcode Scanner?</string>
<string name="message_install_barcode_scanner">Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner?</string>
<string name="insert_bus_stop_number">Numero fermata</string>
<string name="insert_bus_stop_name">Nome fermata</string>
<string name="insert_bus_stop_number_error">Inserisci il numero della fermata</string>
<string name="insert_bus_stop_name_error">Inserisci il nome della fermata</string>
<string name="network_error">Verifica l\'accesso ad Internet!</string>
<string name="no_bus_stop_have_this_name">Sembra che nessuna fermata abbia questo nome</string>
<string name="no_arrivals_stop">Nessun passaggio trovato alla fermata</string>
+ <string name="searching_arrivals_fmt">Ricerca arrivi da %1$s</string>
+
<string name="parsing_error">Errore di lettura del sito 5T/GTT (dannato sito!)</string>
<string name="passages">Fermata: %1$s</string>
<string name="line">Linea</string>
<string name="lines">Linee</string>
<string name="urban_lines">Linee urbane</string>
<string name="extraurban_lines">Linee extraurbane</string>
<string name="turist_lines">Linee turistiche</string>
<string name="direction_duep">Direzione:</string>
<string name="no_lines_found">Nessuna linea in questa categoria</string>
<string name="no_lines_found_query">Nessuna linea corrisponde alla ricerca</string>
<string name="search_box_lines_suggestion_filter">Filtra per nome</string>
<string name="line_fill">Linea: %1$s</string>
<string name="lines_fill">Linee: %1$s</string>
<string name="results">Scegli la fermata…</string>
<string name="no_passages">Nessun passaggio</string>
<string name="no_qrcode">Nessun QR code trovato, prova ad usare un\'altra app</string>
<string name="action_favorites">Preferiti</string>
<string name="action_help">Aiuto</string>
<string name="action_about">Informazioni</string>
<string name="action_about_more">Più informazioni</string>
<string name="action_wiki">Contribuisci</string>
<string name="hack_url">https://gitpull.it/w/librebusto/it/</string>
<string name="action_source">Codice sorgente</string>
<string name="action_licence">Licenza</string>
<string name="action_author">Incontra l\'autore</string>
<string name="added_in_favorites">Fermata aggiunta ai preferiti</string>
<string name="cant_add_to_favorites">Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)!</string>
<string name="title_activity_favorites">Preferiti</string>
<string name="title_activity_map">Mappa</string>
<string name="tip_add_favorite">Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista!</string>
<string name="action_remove_from_favourites">Rimuovi</string>
<string name="action_rename_bus_stop_username">Rinomina</string>
<string name="dialog_rename_bus_stop_username_title">Rinomina fermata</string>
<string name="dialog_rename_bus_stop_username_reset_button">Reset</string>
<string name="about_activity">Informazioni</string>
<string name="howDoesItWork"><b>Tocca la stella</b> per aggiungere la fermata ai preferiti\n\n<b>Come leggere gli orari:</b> \n<b>12:56*</b> Orario in tempo reale\n<b>12:56</b> Orario programmato\n\n<b>Trascina giù per aggiornare</b> l\'orario. \n<b>Tocca a lungo</b> su <b>Fonte Orari</b> per cambiare sorgente degli orari di arrivo</string>
<string name="hint_button">OK!</string>
<string name="about_history_top"><![CDATA[
<h1>Benvenuto!</h1>
<p>Grazie per aver scelto BusTO, un\'app <b>open source</b> e <b>indipendente</b> da GTT/5T, per spostarsi a Torino attraverso <b>software libero</b>!</p>
<p>BusTO rispetta la tua <b>privacy</b> non raccogliendo nessun dato sull\'utilizzo, ed è <b>leggera</b> e senza <b>pubblicità</b>!</p>
<br>
<p>Qui puoi trovare più informazioni e link riguardo al progetto.</p>
<br>
<h2>Schermata iniziale</h2>
<p>Se vuoi rivedere la schermata iniziale, usa il pulsante qui sotto:</b>
]]></string>
<string name="about_channel">
<![CDATA[
<h2>Notizie e aggiornamenti</h2>
<p>Nel canale Telegram puoi trovare informazioni sugli ultimi aggiornamenti dell\'app</p>
]]></string>
<string name="about_how">
<![CDATA[
<h2>Ma come funziona?</h2>
<p>Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito <b>www.gtt.to.it</b>, <i>www.5t.torino.it</i>, <b>muoversiatorino.it</b> "per uso personale" e altre fonti Open Data (aperto.comune.torino.it).</p>
<br>
<p>Ingredienti:<br>
- <b>Fabio Mazza</b> attuale rockstar developer anziano.<br>
- <b>Andrea Ugo</b> attuale rockstar developer in formazione.<br>
- <b>Silviu Chiriac</b> designer del logo 2021.<br>
- <b>Marco M</b> formidabile tester e cacciatore di bug.<br>
- <b>Ludovico Pavesi</b> ex rockstar developer anziano asd.<br>
- <b>Valerio Bozzolan</b> attuale manutentore.<br>
- <b>Marco Gagino</b> apprezzato ex collaboratore, ideatore icona e grafica.<br>
- <b>JSoup</b> libreria per "<i>web scaping</i>".<br>
- <b>Google</b> icone e librerie di supporto e design.<br>
- Altre icone da <b>Bootstrap</b>, <b>Feather</b> e <b>Hero Icons</b><br>
- Tutti i contributori e i beta tester!
</p>
<br>
Se vuoi avere più informazioni tecniche e contribuire allo sviluppo, usa i pulsanti qui sotto!</b>
]]></string>
<string name="about_history_bottom"><![CDATA[
<h2>Licenze</h2>
<p>L\'app e il relativo codice sorgente sono distribuiti sotto la licenza <i>GNU General Public License v3</i> (https://www.gnu.org/licenses/gpl-3.0.html).
Ciò <b>significa</b> che puoi usare, studiare, migliorare e ricondividere quest\'app con <b>qualunque mezzo</b> e per <b>qualsiasi scopo</b>: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan e agli altri autori del codice dell\'app.
</p>
<br>
<h2>Note</h2>
<p>Quest\'applicazione è rilasciata <b>nella speranza che sia utile a tutti</b> ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.</p>
<p>Tutti i dati utilizzati dall\'app provengono <b>direttamente</b> da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.</p>
<p>Buon utilizzo! :)</p>
]]></string>
<string name="query_too_short">Nome troppo corto, digita più caratteri e riprova</string>
<string name="route_towards_destination">%1$s verso %2$s</string>
<string name="route_towards_unknown">%s (destinazione sconosciuta)</string>
<string name="internal_error">Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T</string>
<string name="action_view_on_map">Visualizza sulla mappa</string>
<string name="cannot_show_on_map_no_activity">Non trovo un\'applicazione dove mostrarla</string>
<string name="cannot_show_on_map_no_position">Posizione della fermata non trovata</string>
<string name="nearby_stops_message">Fermate vicine</string>
<string name="position_searching_message">Ricerca della posizione</string>
<string name="no_stops_nearby">Nessuna fermata nei dintorni</string>
<string name="main_menu_pref">Preferenze</string>
<string name="database_update_msg_inapp">Aggiornamento del database…</string>
<string name="database_update_msg_notif">Aggiornamento del database</string>
<string name="database_update_req">Aggiornamento database forzato</string>
<string name="database_update_req_descr">Tocca per aggiornare ora il database</string>
<string name="pref_num_elements">Numero minimo di fermate</string>
<string name="num_stops_nearby_not_number">Il numero di fermate da ricercare non è valido</string>
<string name="invalid_number">Valore errato, inserisci un numero</string>
<string name="title_activity_settings">Impostazioni</string>
<string name="settings_search_radius">Distanza massima di ricerca (m)</string>
<string name="settings_experimental">Funzionalità sperimentali</string>
<string name="action_settings">Impostazioni</string>
<string name="general_settings">Generali</string>
<string name="pref_recents_group_title">Fermate recenti</string>
<string name="settings_group_general">Impostazioni generali</string>
<string name="settings_group_database">Gestione del database</string>
<string name="settings_reset_database">Comincia aggiornamento manuale del database</string>
<string name="enable_position_message_map">Consenti l\'accesso alla posizione per mostrarla sulla mappa</string>
<string name="enable_position_message_nearby">Consenti l\'accesso alla posizione per mostrare le fermate vicine</string>
<string name="enableGpsText">Abilitare il GPS</string>
<string name="bus_arriving_at">arriva alle</string>
<string name="arrivals_card_at_the_stop">alla fermata</string>
<string name="show_arrivals">Mostra arrivi</string>
<string name="show_stops">Mostra fermate</string>
<string name="nearby_arrivals_message">Arrivi qui vicino</string>
<string name="removed_from_favorites">Fermata rimossa dai preferiti</string>
<!--
Mixed button strings
!-->
<string name="open_telegram">Canale telegram</string>
<string name="show_introduction">Mostra introduzione</string>
<!--
Map view buttons strings
!-->
<string name="bt_center_map_description">La mia posizione</string>
<string name="bt_follow_me_description">Segui posizione</string>
<string name="enable_position">Attiva o disattiva posizione</string>
<string name="location_enabled">Posizione attivata</string>
<string name="location_disabled">Posizione disattivata</string>
<string name="map_location_disabled_device">La posizione è disabilitata sul dispositivo</string>
<!--
Arrival times sources
!-->
<string name="times_source_fmt">Fonte orari: %1$s</string>
<string name="fivetapifetcher">App GTT</string>
<string name="gttjsonfetcher">Sito GTT</string>
<string name="fivetscraper">Sito 5T Torino</string>
<string name="source_mato">App Muoversi a Torino</string>
<string name="undetermined_source">Sconosciuta</string>
<string name="arrival_times_choice_title">Fonti orari di arrivo</string>
<string name="arrival_times_choice_explanation">Scegli le fonti di orari da usare</string>
<string name="arrival_source_changing">Cambiamento sorgente orari…</string>
<string name="change_arrivals_source_message">Premi a lungo per cambiare la sorgente degli orari</string>
<string name="no_passages_title">Nessun passaggio per le linee:</string>
<string name="default_notification_channel_description">Canale default delle notifiche</string>
<string name="database_notification_channel">Operazioni sul database</string>
<string name="database_notification_channel_desc">Informazioni sul database (aggiornamento)</string>
<string name="live_position_service_name">BusTO - posizioni in tempo reale</string>
<string name="live_positions_notification_channel">Posizioni in tempo reale</string>
<string name="live_positions_notification_channel_desc">Attività del servizio delle posizioni in tempo reale</string>
<string name="mqtt_notification_text">Servizio posizioni MaTO in tempo reale attivo</string>
<string name="db_trips_download_message">Download dei trip dal server MaTO</string>
<string name="too_many_permission_asks">Chiesto troppe volte per il permesso %1$s</string>
<string name="permission_storage_maps_msg">Non si può usare questa funzionalità senza il permesso di archivio!</string>
<string name="storage_permission">di archivio</string>
<string name="message_crash">Un bug ha fatto crashare l\'app!
\nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug!
\nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash.
</string>
<string name="acra_email_message">L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse:</string>
<string name="nav_arrivals_text">Arrivi</string>
<string name="nav_map_text">Mappa</string>
<string name="nav_favorites_text">Preferiti</string>
<string name="drawer_open">Apri drawer</string>
<string name="drawer_close">Chiudi drawer</string>
<string name="experiments">Esperimenti</string>
<string name="donate_now">Offrici un caffè</string>
<string name="map">Mappa</string>
<string name="stop_search_view_title">Ricerca fermate</string>
<string name="app_version">Versione app</string>
<string name="arrival_times">Orari di arrivo</string>
<!-- Preferences -->
<string name="requesting_db_update">Richiesto aggiornamento del database</string>
<string name="downloading_data_mato">Download dati dal server MaTO</string>
<string name="pref_directions_capitalize">Mostra direzioni in maiuscolo</string>
<string name="directions_capitalize_no_change">Non cambiare</string>
<string name="directions_capitalize_everything">Tutto in maiuscolo</string>
<string name="directions_capitalize_first_letter">Solo prima lettera maiuscola</string>
<string name="pref_lines_click_msg">Mostra arrivi quando tocchi una fermata</string>
<string name="pref_experimental_msg">Abilita esperimenti</string>
<string name="pref_shown_startup">Schermata da mostrare all\'avvio</string>
<string name="pref_shown_startup_def_desc">Tocca per cambiare</string>
<string name="positions_source_pref_title">Fonte posizioni in tempo reale di bus e tram</string>
<string name="positions_source_mato_descr">MaTO (aggiornate più spesso, può non funzionare)</string>
<string name="positions_source_gtfsrt_descr">GTFS RT (più stabile)</string>
<string name="favorites_line_add">Linea aggiunta ai preferiti</string>
<string name="favorites_line_remove">Linea rimossa dai preferiti</string>
<string name="favorites_lines">Preferite</string>
<!-- lines -->
<string name="long_press_stop_4_options">Tocca a lungo la fermata per le opzioni</string>
<string name="map_style_pref_title">Stile della mappa</string>
<string name="map_style_versatiles">Versatiles (vettoriale)</string>
<string name="map_style_legacy_raster">OSM Legacy (raster, più leggera)</string>
<string name="remove_all_trips">Rimuovi i dati dei trip (libera spazio)</string>
<string name="all_trips_removed">Tutti i trip GTFS sono rimossi dal database</string>
<!-- tutorial -->
<string name="tutorial_action">Mostra introduzione</string>
<string name="tutorial_first"><![CDATA[Grazie per aver installato BusTO, l\'app gratuita, libera e <b>open source</b> per il trasporto pubblico di Torino. Stai usando un\'app <b>indipendente</b>, senza pubblicità e senza nessun tracciamento.]]></string>
<string name="tutorial_search"><![CDATA[Puoi cercare gli orari d\'arrivo ad una fermata inserendo il numero nella casella di ricerca.<br>
Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]>
</string>
<string name="tutorial_arrivals">
<![CDATA[Una volta trovata una fermata, puoi aggiungerla ai <b>preferiti</b> toccando la stella a fianco del nome.]]>
</string>
<string name="tutorial_stops">
<![CDATA[Con la posizione abilitata, puoi vedere le <b>fermate più vicine</b> a te direttamente nella schermata principale...]]>
</string>
<string name="tutorial_map">
<![CDATA[...oppure puoi trovarle sulla mappa, assieme alle <b>posizioni in tempo reale</b> dei bus e tram (in <font color="#2F59CC">blu</font>)]]>
</string>
<string name="tutorial_line">
<![CDATA[Scopri il percorso e le fermate della linea che preferisci, insieme alle posizioni in tempo reale dei bus/tram che la servono. Ricorda di scegliere la direzione che ti interessa!]]>
</string>
<string name="tutorial_menu">
<![CDATA[Tutte queste funzioni si trovano nel menu laterale.<br>
Guarda nelle <b>Impostazioni</b> per personalizzare l\'app come preferisci, e su <b>Informazioni</b> per sapere di più sull\'app e il team di sviluppo.]]>
</string>
<string name="close_tutorial">Capito, chiudi introduzione</string>
<string name="close_tutorial_short">Chiudi introduzione</string>
<string name="tutorial_permissions"><![CDATA[
L\'app usa le notifiche per fornire informazioni sullo stato delle attività.
Tocca il bottone per abilitarle.
]]></string>
<string name="grant_location_permission">Abilita accesso alla posizione</string>
<string name="location_permission_granted">Accesso alla posizione abilitato</string>
<string name="location_permission_not_granted">Accesso alla posizione non consentito dall\'utente</string>
<string name="grant_notification_permission">Abilita notifiche</string>
<string name="notification_permission_granted">Notifiche abilitate</string>
<!-- BACKUP -->
<string name="backup_restore">Backup e ripristino</string>
<string name="backup_activity">Importa / esporta dati</string>
<string name="saved_data">Dati salvati</string>
<string name="btn_backup_message">Salva backup</string>
<string name="btn_load_backup_message">Importa i dati dal backup</string>
<string name="data_imported_backup">Backup importato</string>
<string name="message_check_at_least_one">Seleziona almeno un elemento da importare!</string>
<string name="load_file_favorites">Importa preferiti dal backup</string>
<string name="load_preferences">Importa preferenze dal backup</string>
<string name="no_map_app_to_show_stop">Nessuna app disponibile per mostrare la fermata!</string>
<string name="destination_unknown">Destinazione sconosciuta</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 629be30..a481815 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,361 +1,362 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">BusTO</string>
<string name="app_name_full" translatable="false">Libre BusTO</string>
<string name="app_name_debug" translatable="false">BusTO dev</string>
<string name="app_name_gitpull" translatable="false">BusTO git</string>
<string name="app_description">You\'re using the latest in technology when it comes to respecting your privacy.
</string>
<string name="search">Search</string>
<string name="qrcode">Scan QR Code</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="title_barcode_scanner_install">Install Barcode Scanner?</string>
<string name="message_install_barcode_scanner">This application requires an app to scan the QR codes. Would you like
to install Barcode Scanner now?
</string>
<string name="insert_bus_stop_number">Bus stop number</string>
<string name="insert_bus_stop_name">Bus stop name</string>
<string name="insert_bus_stop_number_error">Insert bus stop number</string>
<string name="insert_bus_stop_name_error">Insert bus stop name</string>
<string name="route_towards_destination">%1$s towards %2$s</string>
<string name="route_towards_unknown">%s (unknown destination)</string>
<string name="network_error">Verify your Internet connection!</string>
- <string name="no_bus_stop_have_this_name">Seems that no bus stop have this name</string>
+ <string name="no_bus_stop_have_this_name">Seems that no bus stop has this name</string>
<string name="no_arrivals_stop">No arrivals found for this stop</string>
<string name="parsing_error">Error parsing the 5T/GTT website (damn site!)</string>
<string name="query_too_short">Name too short, type more characters and retry
</string> <!-- TODO: carry out experiments to determine the best wording for this message and publish a paper with the findings -->
<string name="passages">Arrivals at: %1$s</string>
<string name="results">Choose the bus stop…</string>
<string name="line">Line</string>
<string name="lines">Lines</string>
<string name="urban_lines">Urban lines</string>
<string name="extraurban_lines">Extra urban lines</string>
<string name="turist_lines">Tourist lines</string>
<string name="no_lines_found">No lines found in this category</string>
<string name="no_lines_found_query">No lines match the searched name</string>
<string name="direction_duep">Destination:</string>
<string name="lines_fill">Lines: %1$s</string>
<string name="line_fill">Line: %1$s</string>
<string name="no_passages">No timetable found</string>
<string name="no_qrcode">No QR code found, try using another app to scan</string>
<string name="internal_error">Unexpected internal error, cannot extract data from GTT/5T website</string>
<string name="action_help">Help</string>
<string name="action_about">About the app</string>
<string name="action_about_more">More about</string>
<string name="action_wiki">Contribute</string>
<string name="hack_url">https://gitpull.it/w/librebusto/en/</string>
<string name="action_source">Source code</string>
<string name="action_licence">Licence</string>11
<string name="action_author">Meet the author</string>
<string name="added_in_favorites">Bus stop is now in your favorites</string>
<string name="removed_from_favorites">Bus stop removed from your favorites</string>
<string name="favorites_line_add">Added line to favorites</string>
<string name="favorites_line_remove">Remove line from favorites</string>
<string name="favorites_lines">Favorites</string>
<string name="action_favorites">Favorites</string>
<string name="title_activity_favorites">Favorites</string>
<string name="title_activity_map">Map</string>
<string name="tip_add_favorite">No favorites? Arghh! Press on a bus stop star to populate this list!</string>
<string name="action_remove_from_favourites">Delete</string>
<string name="action_rename_bus_stop_username">Rename</string>
<string name="dialog_rename_bus_stop_username_title">Rename the bus stop</string>
<string name="dialog_rename_bus_stop_username_reset_button">Reset</string>
<string name="about_activity">About the app</string>
<string name="howDoesItWork">
<b>Tap the star</b>
to add the bus stop to the favourites\n\n<b>How to read timelines:</b>\n<b>&#160;&#160;&#160;12:56*</b> Real-time
arrivals\n<b>&#160;&#160;&#160;12:56</b> &#160; Scheduled arrivals\n\n<b>Pull down to refresh</b> the timetable
\n <b>Long press on Arrivals source</b> to change the source of the arrival times
</string>
<string name="hint_button">GOT IT!</string>
<string name="arrival_times">Arrival times</string>
<string name="no_passages_title">No arrivals found for lines:</string>
<string name="about_history_top">
<![CDATA[
<h1>Welcome!</h1>
<p>Thanks for using BusTO, an <b>open source</b> and <b>independent</b> app useful to move around Torino using a <b>Free/Libre software</b>.</p>
<br>
<p>Why use this app?</p>
<p>
- You\'ll never be <b>tracked</b><br>
- You\'ll never see boring <b>ads</b><br>
- We\'ll always respect your <b>privacy</b><br>
- Moreover, it\'s lightweight!<br>
</p>
<br>
<h2>Introductory tutorial</h2>
<p>If you want to see the introduction again, use the button below:</p>
]]></string>
<string name="about_channel">
<![CDATA[
<h2>News and Updates</h2>
<p>On the Telegram channel, you can find information about the latest app updates</p>
]]></string>
<string name="about_how">
<![CDATA[
<h2>How does it work?</h2>
<p>This app is able to do all the amazing things it does by pulling data from <b>www.gtt.to.it</b>, <b>www.5t.torino.it</b> or <b>muoversiatorino.it</b> "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.</p>
<br>
<p>The work of several people is behind this app, in particular:<br>
- <b>Fabio Mazza</b>, current senior rockstar developer.<br>
- <b>Andrea Ugo</b>, current junior rockstar developer.<br>
- <b>Silviu Chiriac</b>, designer of the 2021 logo.<br>
- <b>Marco M</b>, rockstar tester and bug hunter.<br>
- <b>Ludovico Pavesi</b>, previous senior rockstar developer (asd).<br>
- <b>Valerio Bozzolan</b>, maintainer and infrastructure (sponsor).<br>
- <b>Marco Gagino</b>, contributor and first icon creator.<br>
- <b>JSoup</b> web scraper library.<br>
- <b>makovkastar</b> floating buttons.<br>
- <b>Google</b> for icons and support and design libraries.<br>
- Other icons from <b>Bootstrap</b>, <b>Feather</b>, and <b>Hero Icons</b>.<br>
- All the contributors, and the beta testers, too!
</p>
<br>
If you want more technical information or to contribute to development, use the buttons below!</b>
]]></string>
<string name="about_history_bottom">
<![CDATA[
<h2>Licenses</h2>
<p>The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the <i>GNU General Public License v3+</i>).
So everyone is allowed to use, to study, to improve and to share this app by <b>any kind of means</b> and for <b>any purpose</b>: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.</p>
<br>
<h2>Notes</h2>
<p>This app has been developed with the <b>hope to be useful to everyone</b>, but comes without ANY warranty of any kind.</p>
<p>The data used by the app comes <b>directly</b> from GTT and other public agencies: if you find any errors, please take it up to them, not to us.</p>
<p>This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.</p>
<p>Now you can hack public transport, too! :)</p>
]]>
</string>
<string name="cant_add_to_favorites">Cannot add to favorites (storage full or corrupted database?)!</string>
<string name="action_view_on_map">View on a map</string>
<string name="cannot_show_on_map_no_activity">Cannot find any application to show it in</string>
<string name="cannot_show_on_map_no_position">Cannot find the position of the stop</string>
<string name="list_fragment_debug" translatable="false">ListFragment - BusTO</string>
<string name="mainSharedPreferences" translatable="false">it.reyboz.bustorino.preferences</string>
<string name="databaseUpdatingPref" translatable="false">db_is_updating</string>
<!-- Settings -->
<string name="nearby_stops_message">Nearby stops</string>
<string name="nearby_arrivals_message">Nearby connections</string>
<string name="app_version">App version</string>
<string name="num_stops_nearby_not_number">The number of stops to show in the recent stops is invalid</string>
<string name="invalid_number">Invalid value, put a valid number</string>
<string name="position_searching_message">Finding location</string>
<string name="no_stops_nearby">No stops nearby</string>
<string name="pref_num_elements">Minimum number of stops</string>
<string name="main_menu_pref">Preferences</string>
<string name="title_activity_settings">Settings</string>
<string name="action_settings">Settings</string>
<string name="general_settings">General</string>
<string name="settings_experimental">Experimental features</string>
<string name="settings_search_radius">Maximum distance (meters)</string>
<string name="pref_recents_group_title">Recent stops</string>
<string name="settings_group_general">General settings</string>
<string name="settings_group_database">Database management</string>
<string name="settings_reset_database">Launch manual database update</string>
<string name="enable_position_message_map">Allow access to location to show it on the map</string>
<string name="enable_position_message_nearby">Allow access to location to show stops nearby</string>
<string name="enableGpsText">Please enable location on the device</string>
<string name="database_update_msg_inapp">Database update in progress&#8230;</string>
<string name="database_update_msg_notif">Updating the database</string>
<string name="database_update_req">Force database update</string>
<string name="database_update_req_descr">Touch to update the app database now</string>
<string name="bus_arriving_at">is arriving at</string>
<string name="arrivals_card_at_the_stop">at the stop</string>
<string name="two_strings_format" translatable="false">%1$s - %2$s</string>
<string name="show_arrivals">Show arrivals</string>
<string name="show_stops">Show stops</string>
<!--
Mixed button strings
!-->
<string name="open_telegram">Join Telegram channel</string>
<string name="show_introduction">Show introduction</string>
<!--
Map view buttons strings
!-->
<string name="bt_center_map_description">Center on my location</string>
<string name="bt_follow_me_description">Follow me</string>
<string name="enable_position">Enable or disable location</string>
<string name="location_enabled">Location enabled</string>
<string name="location_disabled">Location disabled</string>
<string name="map_location_disabled_device">Location is disabled on device</string>
<!--
Arrival times sources
!-->
<string name="times_source_fmt">Arrivals source: %1$s</string>
+ <string name="searching_arrivals_fmt">Loading arrivals from %1$s</string>
<string name="fivetapifetcher">GTT App</string>
<string name="gttjsonfetcher">GTT Website</string>
<string name="fivetscraper">5T Torino website</string>
<string name="source_mato">Muoversi a Torino app</string>
<string name="undetermined_source">Undetermined</string>
<string name="arrival_source_changing">Changing arrival times source…</string>
<string name="change_arrivals_source_message">Long press to change the source of arrivals</string>
<string-array name="arrival_times_source_list">
<item>@string/source_mato</item>
<item>@string/fivetapifetcher</item>
<item>@string/gttjsonfetcher</item>
<item>@string/fivetscraper</item>
</string-array>
<string name="arrival_times_choice_title">Sources of arrival times</string>
<string name="arrival_times_choice_explanation">Select which sources of arrival times to use</string>
<!--
Notifications
-->
<string name="default_notification_channel" translatable="false">Default</string>
<string name="default_notification_channel_description">Default channel for notifications</string>
<string name="database_notification_channel">Database operations</string>
<string name="database_notification_channel_desc">Updates of the app database</string>
<string name="live_position_service_name">BusTO - live position service</string>
<string name="live_positions_notification_channel">Live positions</string>
<string name="live_positions_notification_channel_desc">Showing activity related to the live positions service</string>
<string name="mqtt_notification_text">MaTO live bus positions service is running</string>
<string name="db_trips_download_message">Downloading trips from MaTO server</string>
<string name="too_many_permission_asks">Asked for %1$s permission too many times</string>
<string name="permission_storage_maps_msg">Cannot use the map with the storage permission!</string>
<string name="storage_permission">storage</string>
<string name="message_crash">The application has crashed because you encountered a bug.
\nIf you want, you can help the developers by sending the crash report via email.
\nNote that no sensitive data is contained in the report, just small bits of info on your phone and app
configuration/state.
</string>
<string name="acra_email_message">The application crashed and the crash report is in the attachments. Please
describe what you were doing before the crash: \n
</string>
<string name="nav_arrivals_text">Arrivals</string>
<string name="nav_map_text">Map</string>
<string name="nav_favorites_text">Favorites</string>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
<string name="experiments">Experiments</string>
<string name="donate_now">Buy us a coffee</string>
<string name="map">Map</string>
<string name="stop_search_view_title">Search by stop</string>
<string name="search_box_lines_suggestion_filter">Filter by name</string>
<string name="requesting_db_update">Launching database update</string>
<string name="downloading_data_mato">Downloading data from MaTO server</string>
<!-- preferences -->
<string name="pref_directions_capitalize">Capitalize directions</string>
<string-array name="directions_capitalize">
<item>@string/directions_capitalize_no_change</item>
<item>@string/directions_capitalize_everything</item>
<item>@string/directions_capitalize_first_letter</item>
</string-array>
<string name="directions_capitalize_no_change">Do not change arrivals directions</string>
<string name="directions_capitalize_everything">Capitalize everything</string>
<string name="directions_capitalize_first_letter">Capitalize only first letter</string>
<array name="directions_capitalize_keys">
<item>KEEP</item>
<item>CAPITALIZE_ALL</item>
<item>CAPITALIZE_FIRST</item>
</array>
<string name="pref_shown_startup">Section to show on startup</string>
<string name="pref_shown_startup_def_desc">Touch to change it</string>
<string name="pref_lines_click_msg">Show arrivals touching on stop</string>
<string name="pref_experimental_msg">Enable experiments</string>
<!-- lines -->
<string name="long_press_stop_4_options">Long press the stop for options</string>
<array name="first_screen_shown">
<item>@string/nav_arrivals_text</item>
<item>@string/nav_favorites_text</item>
<item>@string/nav_map_text</item>
<item>@string/lines</item>
</array>
<string name="positions_source_pref_title">Source of real time positions for buses and trams</string>
<string name="positions_source_mato_descr">MaTO (updated more frequently, might be offline)</string>
<string name="positions_source_gtfsrt_descr">GTFS RT (more stable, less frequently updated)</string>
<array name="positions_source_sel">
<item>@string/positions_source_mato_descr</item>
<item>@string/positions_source_gtfsrt_descr</item>
</array>
<string name="map_style_pref_title">Style of the map</string>
<string name="map_style_versatiles">Versatiles (vector)</string>
<string name="map_style_legacy_raster">OSM legacy (raster, lighter)</string>
<array name="map_style_pref_categories">
<item>@string/map_style_versatiles</item>
<item>@string/map_style_legacy_raster</item>
</array>
<string name="remove_all_trips">Remove trips data (free up space)</string>
<string name="all_trips_removed">All GTFS trips have been removed from the database</string>
<!-- Tutorial strings -->
<string name="tutorial_action">Show tutorial</string>
<string name="tutorial_first">
<![CDATA[Thank you for installing BusTO, the free and <b>open source</b> app for Turin public transport. This is an <b>independent</b> app, with no ads and no tracking whatsoever.]]> </string>
<string name="tutorial_search"> <![CDATA[You can search for the arrival times at a bus stop by inserting the number in the top, or scanning a QR code when you are at the stop
touching the icon on the left ]]>
</string>
<string name="tutorial_arrivals">
<![CDATA[Once you have found a bus stop, you can save it in the <b>favorites</b> by touching the star next to its name]]>
</string>
<string name="tutorial_stops">
<![CDATA[You can see the stops near your position directly in the main screen...]]>
</string>
<string name="tutorial_map">
<![CDATA[...or you can find them in the map, along with the real-time positions of the buses running in the city (in <font color="#2F59CC">blue</font>)]]>
</string>
<string name="tutorial_line">
<![CDATA[You can also look at the line you want and see where the stops are, together with the vehicles serving the line at the moment. Remember to choose the direction you\'re interested in!]]>
</string>
<string name="tutorial_menu">
<![CDATA[You can find all of this from the lateral menu.
Look in the <b>Settings</b> to customize the app behaviour, and in the <b>About the app</b> section if you want to know more about the app and the developers.]]>
</string>
<string name="tutorial_permissions">
<![CDATA[The app needs the <b>Notifications</b> permission to show the information about background processing.
Press the button below to grant it]]>
</string>
<string name="grant_location_permission">Grant location permission</string>
<string name="location_permission_granted">Location permission granted</string>
<string name="location_permission_not_granted">Location permission has not been granted</string>
<string name="close_tutorial">OK, close the tutorial</string>
<string name="close_tutorial_short">Close the tutorial</string>
<string name="grant_notification_permission">Enable notifications</string>
<string name="notification_permission_granted">Notifications enabled</string>
<!-- BACKUP -->
<string name="backup_restore">Backup and restore</string>
<string name="backup_activity">Import/export preferences</string>
<string name="saved_data">Data saved</string>
<string name="btn_backup_message">Backup to file</string>
<string name="btn_load_backup_message">Import data from backup</string>
<string name="data_imported_backup">Backup has been imported</string>
<string name="message_check_at_least_one">Check at least one item to import!</string>
<string name="load_file_favorites">Import favorites from backup</string>
<string name="load_preferences">Import preferences from backup</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="no_map_app_to_show_stop">No map app present to show the stop!</string>
<string name="showing_same_direction">Direction is already shown</string>
<string name="destination_loading">Loading destination…</string>
<string name="destination_unknown">Destination unknown</string>
</resources>

File Metadata

Mime Type
text/x-diff
Expires
Wed, Jun 24, 21:44 (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1975695
Default Alt Text
(270 KB)

Event Timeline