diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8cf2fbd..58daede 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,124 +1,125 @@
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 21ab572..62a1095 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,145 +1,145 @@
buildscript {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
google()
}
ext {
//multidex
multidex_version = "2.0.1"
//libraries versions
- fragment_version = "1.3.6"
- activity_version = "1.3.1"
- appcompat_version = "1.3.1"
- preference_version = "1.1.1"
- work_version = "2.5.0"
+ fragment_version = "1.4.1"
+ activity_version = "1.4.0"
+ appcompat_version = "1.4.1"
+ preference_version = "1.2.0"
+ work_version = "2.7.1"
acra_version = "5.7.0"
- lifecycle_version = "2.3.1"
+ lifecycle_version = "2.4.1"
arch_version = "2.1.0"
- room_version = "2.3.0"
+ room_version = "2.4.1"
//kotlin
kotlin_version = '1.6.0'
coroutines_version = "1.5.0"
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.3'
+ classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
google()
mavenCentral()
}
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
- compileSdkVersion 30
+ compileSdkVersion 31
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "it.reyboz.bustorino"
minSdkVersion 16
- targetSdkVersion 30
+ targetSdkVersion 31
versionCode 38
versionName "1.16.2"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-dev"
}
}
lintOptions {
abortOnError false
}
repositories {
jcenter()
mavenLocal()
}
dependencies {
//new libraries
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.activity:activity:$activity_version"
- implementation "androidx.annotation:annotation:1.2.0"
+ implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation "androidx.preference:preference:$preference_version"
implementation "androidx.work:work-runtime:$work_version"
- implementation "com.google.android.material:material:1.4.0"
- implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
- implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
+ implementation "com.google.android.material:material:1.5.0"
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1'
implementation 'com.android.volley:volley:1.2.1'
implementation 'org.osmdroid:osmdroid-android:6.1.10'
// ACRA
implementation "ch.acra:acra-mail:$acra_version"
implementation "ch.acra:acra-dialog:$acra_version"
// google transit realtime
implementation 'com.google.protobuf:protobuf-java:3.14.0'
// ViewModel
- implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"
// Room components
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
androidTestImplementation "androidx.room:room-testing:$room_version"
//multidex - we need this to build the app
implementation "androidx.multidex:multidex:$multidex_version"
implementation 'de.siegmar:fastcsv:2.0.0'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7dc96cb..599f4da 100755
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sat Apr 24 16:03:07 CEST 2021
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/src/it/reyboz/bustorino/data/FavoritesLiveData.java b/src/it/reyboz/bustorino/data/FavoritesLiveData.java
index 4ca1149..07ce144 100644
--- a/src/it/reyboz/bustorino/data/FavoritesLiveData.java
+++ b/src/it/reyboz/bustorino/data/FavoritesLiveData.java
@@ -1,214 +1,219 @@
/*
BusTO - Data components
Copyright (C) 2021 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
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> implements CustomAsyncQueryHandler.AsyncQueryListener {
- private static final String TAG = "FavoritesLiveData";
+ 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 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()");
if (!forceQuery){
if (getValue()!= null){
//Data already loaded
return;
}
}
if (isQueryRunning){
//we are waiting for data, we will get an update soon
return;
}
isQueryRunning = true;
queryHandler.startQuery(FAV_TOKEN,null, FAVORITES_URI, UserDB.getFavoritesColumnNamesAsArray, null, null, null);
}
@Override
protected void onActive() {
//Log.d(TAG, "onActive()");
loadData();
}
/**
* Clear the data for the cursor
*/
public void onClear(){
ContentResolver resolver = mContext.getContentResolver();
resolver.unregisterContentObserver(mObserver);
}
@Override
protected void setValue(List 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);
} 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 result = Arrays.asList(NextGenDB.getStopsFromCursorAllFields(cursor));
+ List result = NextGenDB.getStopsFromCursorAllFields(cursor);
cursor.close();
if (result.size() < 1){
// 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());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
Log.d(TAG, "ForceLoadContentObserver.onChange()");
loadData(true);
}
}
}
diff --git a/src/it/reyboz/bustorino/data/NextGenDB.java b/src/it/reyboz/bustorino/data/NextGenDB.java
index 625f537..e500d0e 100644
--- a/src/it/reyboz/bustorino/data/NextGenDB.java
+++ b/src/it/reyboz/bustorino/data/NextGenDB.java
@@ -1,372 +1,370 @@
/*
BusTO (middleware)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.util.Log;
import it.reyboz.bustorino.backend.Route;
import it.reyboz.bustorino.backend.Stop;
import java.util.*;
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 String QUERY_WHERE_ID = StopsTable.COL_ID+" = ?";
private final Context appContext;
public NextGenDB(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
appContext = context.getApplicationContext();
}
@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);
}
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
*
* 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 Stop[] queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
- Stop[] stops = new Stop[0];
+ public synchronized ArrayList queryAllInsideMapView(double minLat, double maxLat, double minLng, double maxLng) {
+ ArrayList stops = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
- //Cursor result=null;
- int count;
-
// 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;
}finally {
db.close();
}
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 Stop[] getStopsFromCursorAllFields(Cursor result){
- int colID = result.getColumnIndex(StopsTable.COL_ID);
- int colName = result.getColumnIndex(StopsTable.COL_NAME);
- int colLocation = result.getColumnIndex(StopsTable.COL_LOCATION);
- int colType = result.getColumnIndex(StopsTable.COL_TYPE);
- int colLat = result.getColumnIndex(StopsTable.COL_LAT);
- int colLon = result.getColumnIndex(StopsTable.COL_LONG);
- int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
+ public static ArrayList 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 colLon = result.getColumnIndex(StopsTable.COL_LONG);
+ final int colLines = result.getColumnIndex(StopsTable.COL_LINES_STOPPING);
int count = result.getCount();
- Stop[] stops = new Stop[count];
+ ArrayList stops = new ArrayList<>(count);
int i = 0;
while(result.moveToNext()) {
final String stopID = result.getString(colID).trim();
final Route.Type type;
if(result.getString(colType) == null) type = Route.Type.BUS;
else type = Route.getTypeFromSymbol(result.getString(colType));
String lines = result.getString(colLines).trim();
String locationSometimesEmpty = result.getString(colLocation);
if (locationSometimesEmpty!= null && locationSometimesEmpty.length() <= 0) {
locationSometimesEmpty = null;
}
- stops[i++] = new Stop(stopID, result.getString(colName), null,
+ stops.add(new Stop(stopID, result.getString(colName), null,
locationSometimesEmpty, type, splitLinesString(lines),
- result.getDouble(colLat), result.getDouble(colLon));
+ result.getDouble(colLat), result.getDouble(colLon))
+ );
}
return stops;
}
/**
* 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 stops){
return 0;
}
public static List 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/src/it/reyboz/bustorino/data/UserDB.java b/src/it/reyboz/bustorino/data/UserDB.java
index a6f19ee..cc0a09c 100644
--- a/src/it/reyboz/bustorino/data/UserDB.java
+++ b/src/it/reyboz/bustorino/data/UserDB.java
@@ -1,320 +1,326 @@
/*
BusTO ("backend" components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.StopsDBInterface;
public class UserDB extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "user.db";
static final String TABLE_NAME = "favorites";
private final Context c; // needed during upgrade
private final static String[] usernameColumnNameAsArray = {"username"};
public final static String[] getFavoritesColumnNamesAsArray = {"ID", "username"};
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 ID = new ArrayList<>();
ArrayList username = new ArrayList<>();
int len;
int len2;
try {
Cursor c = old.getReadableDatabase().rawQuery("SELECT busstop_ID, busstop_username FROM busstop WHERE busstop_isfavorite = 1 ORDER BY busstop_name ASC", new String[] {});
int zero = c.getColumnIndex("busstop_ID");
int one = c.getColumnIndex("busstop_username");
while(c.moveToNext()) {
try {
ID.add(c.getString(zero));
} catch(Exception e) {
// no ID = can't add this
continue;
}
if(c.getString(one) == null || c.getString(one).length() <= 0) {
username.add(null);
} else {
username.add(c.getString(one));
}
}
c.close();
old.close();
} catch(Exception ignored) {
// there's no hope, go ahead and nuke old database.
}
len = ID.size();
len2 = username.size();
if(len2 < len) {
len = len2;
}
if (len > 0) {
try {
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) {
String username = null;
try {
Cursor c = db.query(TABLE_NAME, usernameColumnNameAsArray, "ID = ?", new String[] {stopID}, null, null, null);
if(c.moveToNext()) {
- username = c.getString(c.getColumnIndex("username"));
+ int userNameIndex = c.getColumnIndex("username");
+ if (userNameIndex>=0)
+ username = c.getString(userNameIndex);
}
c.close();
} catch(SQLiteException ignored) {}
return username;
}
/**
* Get all the bus stops marked as favorites
*
* @param db
* @param dbi
* @return
*/
public static List getFavorites(SQLiteDatabase db, StopsDBInterface dbi) {
List l = new ArrayList<>();
Stop s;
String stopID, stopUserName;
try {
Cursor c = db.query(TABLE_NAME, getFavoritesColumnNamesAsArray, null, null, null, null, null, null);
int colID = c.getColumnIndex("ID");
int colUser = c.getColumnIndex("username");
while(c.moveToNext()) {
stopUserName = c.getString(colUser);
stopID = c.getString(colID);
s = dbi.getAllFromID(stopID);
if(s == null) {
// can't find it in database
l.add(new Stop(stopUserName, stopID, null, null, null));
} else {
// setStopName() already does sanity checks
s.setStopUserName(stopUserName);
l.add(s);
}
}
c.close();
} catch(SQLiteException ignored) {}
// comparison rules are too complicated to let SQLite do this (e.g. it outputs: 3234, 34, 576, 67, 8222) and stop name is in another database
Collections.sort(l);
return l;
}
public static void notifyContentProvider(Context context){
context.
getContentResolver().
notifyChange(FAVORITES_URI, null);
}
public static ArrayList getFavoritesFromCursor(Cursor cursor, String[] columns){
List colsList = Arrays.asList(columns);
if (!colsList.contains(getFavoritesColumnNamesAsArray[0]) || !colsList.contains(getFavoritesColumnNamesAsArray[1])){
throw new IllegalArgumentException();
}
ArrayList 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;
}
}
diff --git a/src/it/reyboz/bustorino/fragments/FragmentHelper.java b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
index caf4cd0..d86349f 100644
--- a/src/it/reyboz/bustorino/fragments/FragmentHelper.java
+++ b/src/it/reyboz/bustorino/fragments/FragmentHelper.java
@@ -1,282 +1,285 @@
/*
BusTO (fragments)
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.content.Context;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Fetcher;
import it.reyboz.bustorino.backend.Palina;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.backend.utils;
import it.reyboz.bustorino.middleware.*;
import java.lang.ref.WeakReference;
import java.util.List;
/**
* Helper class to manage the fragments and their needs
*/
public class FragmentHelper {
//GeneralActivity act;
private final FragmentListenerMain listenerMain;
private final WeakReference managerWeakRef;
private Stop lastSuccessfullySearchedBusStop;
//support for multiple frames
private final int secondaryFrameLayout;
private final int primaryFrameLayout;
private final Context context;
public static final int NO_FRAME = -3;
private static final String DEBUG_TAG = "BusTO FragmHelper";
private WeakReference lastTaskRef;
- private SearchRequestType lastTaskType;
private boolean shouldHaltAllActivities=false;
public FragmentHelper(FragmentListenerMain listener, FragmentManager framan, Context context, int mainFrame) {
this(listener,framan, context,mainFrame,NO_FRAME);
}
public FragmentHelper(FragmentListenerMain listener, FragmentManager fraMan, Context context, int primaryFrameLayout, int secondaryFrameLayout) {
this.listenerMain = listener;
this.managerWeakRef = new WeakReference<>(fraMan);
this.primaryFrameLayout = primaryFrameLayout;
this.secondaryFrameLayout = secondaryFrameLayout;
this.context = context.getApplicationContext();
}
/**
* Get the last successfully searched bus stop or NULL
*
* @return the stop
*/
public Stop getLastSuccessfullySearchedBusStop() {
return lastSuccessfullySearchedBusStop;
}
public void setLastSuccessfullySearchedBusStop(Stop stop) {
this.lastSuccessfullySearchedBusStop = stop;
}
public void setLastTaskRef(AsyncTask task) {
this.lastTaskRef = new WeakReference<>(task);
}
/**
* Called when you need to create a fragment for a specified Palina
* @param p the Stop that needs to be displayed
*/
public void createOrUpdateStopFragment(Palina p, boolean addToBackStack){
boolean sameFragment;
ArrivalsFragment arrivalsFragment = null;
if(managerWeakRef.get()==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything");
return;
}
FragmentManager fm = managerWeakRef.get();
if(fm.findFragmentById(primaryFrameLayout) instanceof ArrivalsFragment) {
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout);
//Log.d(DEBUG_TAG, "Arrivals are for fragment with same stop?");
if (arrivalsFragment == null) sameFragment = false;
else sameFragment = arrivalsFragment.isFragmentForTheSameStop(p);
} else {
sameFragment = false;
Log.d(DEBUG_TAG, "We aren't showing an ArrivalsFragment");
}
setLastSuccessfullySearchedBusStop(p);
if (sameFragment){
Log.d("BusTO", "Same bus stop, accessing existing fragment");
arrivalsFragment = (ArrivalsFragment) fm.findFragmentById(primaryFrameLayout);
if (arrivalsFragment == null) sameFragment = false;
}
if(!sameFragment) {
//set the String to be displayed on the fragment
String displayName = p.getStopDisplayName();
if (displayName != null && displayName.length() > 0) {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID,displayName);
} else {
arrivalsFragment = ArrivalsFragment.newInstance(p.ID);
}
String probableTag = ResultListFragment.getFragmentTag(p);
attachFragmentToContainer(fm,arrivalsFragment,new AttachParameters(probableTag, true, addToBackStack));
}
// DO NOT CALL `setListAdapter` ever on arrivals fragment
arrivalsFragment.updateFragmentData(p);
// enable fragment auto refresh
arrivalsFragment.setReloadOnResume(true);
listenerMain.hideKeyboard();
toggleSpinner(false);
}
/**
* Called when you need to display the results of a search of stops
* @param resultList the List of stops found
* @param query String queried
*/
public void createStopListFragment(List resultList, String query, boolean addToBackStack){
listenerMain.hideKeyboard();
StopListFragment listfragment = StopListFragment.newInstance(query);
if(managerWeakRef.get()==null || shouldHaltAllActivities) {
//SOMETHING WENT VERY WRONG
Log.e(DEBUG_TAG, "We are asked for a new stop but we can't show anything");
return;
}
attachFragmentToContainer(managerWeakRef.get(),listfragment,
new AttachParameters("search_"+query, false,addToBackStack));
listfragment.setStopList(resultList);
listenerMain.readyGUIfor(FragmentKind.STOPS);
toggleSpinner(false);
}
/**
* Wrapper for toggleSpinner in Activity
* @param on new status of spinner system
*/
public void toggleSpinner(boolean on){
listenerMain.toggleSpinner(on);
}
/**
* Attach a new fragment to a cointainer
* @param fm the FragmentManager
* @param fragment the Fragment
* @param parameters attach parameters
*/
protected void attachFragmentToContainer(FragmentManager fm,Fragment fragment, AttachParameters parameters){
if(shouldHaltAllActivities) //nothing to do
return;
FragmentTransaction ft = fm.beginTransaction();
int frameID;
if(parameters.attachToSecondaryFrame && secondaryFrameLayout!=NO_FRAME)
// ft.replace(secondaryFrameLayout,fragment,tag);
frameID = secondaryFrameLayout;
else frameID = primaryFrameLayout;
switch (parameters.transaction){
case REPLACE:
ft.replace(frameID,fragment,parameters.tag);
}
if (parameters.addToBackStack)
ft.addToBackStack("state_"+parameters.tag);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE);
if(!fm.isDestroyed() && !shouldHaltAllActivities)
ft.commit();
//fm.executePendingTransactions();
}
public synchronized void setBlockAllActivities(boolean shouldI) {
this.shouldHaltAllActivities = shouldI;
}
public void stopLastRequestIfNeeded(boolean interruptIfRunning){
if(lastTaskRef == null) return;
AsyncTask task = lastTaskRef.get();
if(task!=null){
task.cancel(interruptIfRunning);
}
}
/**
* Wrapper to show the errors/status that happened
* @param res result from Fetcher
*/
public void showErrorMessage(Fetcher.Result res, SearchRequestType type){
//TODO: implement a common set of errors for all fragments
+ if (res==null){
+ Log.e(DEBUG_TAG, "Asked to show result with null result");
+ return;
+ }
Log.d(DEBUG_TAG, "Showing result for "+res);
switch (res){
case OK:
break;
case CLIENT_OFFLINE:
showToastMessage(R.string.network_error, true);
break;
case SERVER_ERROR:
if (utils.isConnected(context)) {
showToastMessage(R.string.parsing_error, true);
} else {
showToastMessage(R.string.network_error, true);
}
case PARSER_ERROR:
default:
showShortToast(R.string.internal_error);
break;
case QUERY_TOO_SHORT:
showShortToast(R.string.query_too_short);
break;
case EMPTY_RESULT_SET:
if (type == SearchRequestType.STOPS)
showShortToast(R.string.no_bus_stop_have_this_name);
else if(type == SearchRequestType.ARRIVALS){
showShortToast(R.string.no_arrivals_stop);
}
break;
case NOT_FOUND:
showShortToast(R.string.no_bus_stop_have_this_name);
break;
}
}
public void showToastMessage(int messageID, boolean short_lenght) {
final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
if (context != null)
Toast.makeText(context, messageID, length).show();
}
private void showShortToast(int messageID){
showToastMessage(messageID, true);
}
enum Transaction{
REPLACE,
}
static final class AttachParameters {
String tag;
boolean attachToSecondaryFrame;
Transaction transaction;
boolean addToBackStack;
public AttachParameters(String tag, boolean attachToSecondaryFrame, Transaction transaction, boolean addToBackStack) {
this.tag = tag;
this.attachToSecondaryFrame = attachToSecondaryFrame;
this.transaction = transaction;
this.addToBackStack = addToBackStack;
}
public AttachParameters(String tag, boolean attachToSecondaryFrame, boolean addToBackStack) {
this.tag = tag;
this.attachToSecondaryFrame = attachToSecondaryFrame;
this.addToBackStack = addToBackStack;
this.transaction = Transaction.REPLACE;
}
}
}
diff --git a/src/it/reyboz/bustorino/fragments/MapFragment.java b/src/it/reyboz/bustorino/fragments/MapFragment.java
index 13b1cae..509958d 100644
--- a/src/it/reyboz/bustorino/fragments/MapFragment.java
+++ b/src/it/reyboz/bustorino/fragments/MapFragment.java
@@ -1,644 +1,645 @@
/*
BusTO - Fragments components
Copyright (C) 2020 Andrea Ugo
Copyright (C) 2021 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
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.core.content.res.ResourcesCompat;
import androidx.preference.PreferenceManager;
import it.reyboz.bustorino.backend.utils;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.api.IMapController;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.DelayedMapListener;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.overlay.FolderOverlay;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.infowindow.InfoWindow;
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
import java.lang.ref.WeakReference;
import java.util.*;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.NextGenDB;
import it.reyboz.bustorino.map.CustomInfoWindow;
import it.reyboz.bustorino.map.LocationOverlay;
import it.reyboz.bustorino.middleware.GeneralActivity;
import it.reyboz.bustorino.util.Permissions;
public class MapFragment extends ScreenBaseFragment {
private static final String TAG = "Busto-MapActivity";
private static final String MAP_CURRENT_ZOOM_KEY = "map-current-zoom";
private static final String MAP_CENTER_LAT_KEY = "map-center-lat";
private static final String MAP_CENTER_LON_KEY = "map-center-lon";
private static final String FOLLOWING_LOCAT_KEY ="following";
public static final String BUNDLE_LATIT = "lat";
public static final String BUNDLE_LONGIT = "lon";
public static final String BUNDLE_NAME = "name";
public static final String BUNDLE_ID = "ID";
public static final String FRAGMENT_TAG="BusTOMapFragment";
private static final double DEFAULT_CENTER_LAT = 45.0708;
private static final double DEFAULT_CENTER_LON = 7.6858;
private static final double POSITION_FOUND_ZOOM = 18.3;
public static final double NO_POSITION_ZOOM = 17.1;
private static final String DEBUG_TAG=FRAGMENT_TAG;
protected FragmentListenerMain listenerMain;
private HashSet shownStops = null;
//the asynctask used to get the stops from the database
private AsyncStopFetcher stopFetcher = null;
private MapView map = null;
public Context ctx;
private LocationOverlay mLocationOverlay = null;
private FolderOverlay stopsFolderOverlay = null;
private Bundle savedMapState = null;
protected ImageButton btCenterMap;
protected ImageButton btFollowMe;
private boolean followingLocation = false;
protected final CustomInfoWindow.TouchResponder responder = new CustomInfoWindow.TouchResponder() {
@Override
public void onActionUp(@NonNull String stopID, @Nullable String stopName) {
if (listenerMain!= null){
listenerMain.requestArrivalsForStopID(stopID);
}
}
};
protected final LocationOverlay.OverlayCallbacks locationCallbacks = new LocationOverlay.OverlayCallbacks() {
@Override
public void onDisableFollowMyLocation() {
updateGUIForLocationFollowing(false);
followingLocation=false;
}
@Override
public void onEnableFollowMyLocation() {
updateGUIForLocationFollowing(true);
followingLocation=true;
}
};
private final ActivityResultLauncher positionRequestLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){
map.getOverlays().remove(mLocationOverlay);
startLocationOverlay(true, map);
if(getContext()==null || getContext().getSystemService(Context.LOCATION_SERVICE)==null)
return;
LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
@SuppressLint("MissingPermission")
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
map.getController().setZoom(POSITION_FOUND_ZOOM);
GeoPoint startPoint = new GeoPoint(userLocation);
setLocationFollowing(true);
map.getController().setCenter(startPoint);
}
}
else Log.w(DEBUG_TAG,"No location permission");
});
public MapFragment() {
}
public static MapFragment getInstance(){
return new MapFragment();
}
public static MapFragment getInstance(double stopLatit, double stopLong, String stopName, String stopID){
MapFragment fragment= new MapFragment();
Bundle args = new Bundle();
args.putDouble(BUNDLE_LATIT, stopLatit);
args.putDouble(BUNDLE_LONGIT, stopLong);
args.putString(BUNDLE_NAME, stopName);
args.putString(BUNDLE_ID, stopID);
fragment.setArguments(args);
return fragment;
}
public static MapFragment getInstance(@NonNull Stop stop){
return getInstance(stop.getLatitude(), stop.getLongitude(), stop.getStopDisplayName(), stop.ID);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//use the same layout as the activity
View root = inflater.inflate(R.layout.activity_map, container, false);
if (getContext() == null){
throw new IllegalStateException();
}
ctx = getContext().getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
map = root.findViewById(R.id.map);
map.setTileSource(TileSourceFactory.MAPNIK);
//map.setTilesScaledToDpi(true);
map.setFlingEnabled(true);
// add ability to zoom with 2 fingers
map.setMultiTouchControls(true);
btCenterMap = root.findViewById(R.id.ic_center_map);
btFollowMe = root.findViewById(R.id.ic_follow_me);
//setup FolderOverlay
stopsFolderOverlay = new FolderOverlay();
//Start map from bundle
if (savedInstanceState !=null)
startMap(getArguments(), savedInstanceState);
else startMap(getArguments(), savedMapState);
//set listeners
map.addMapListener(new DelayedMapListener(new MapListener() {
@Override
public boolean onScroll(ScrollEvent paramScrollEvent) {
requestStopsToShow();
//Log.d(DEBUG_TAG, "Scrolling");
//if (moveTriggeredByCode) moveTriggeredByCode =false;
//else setLocationFollowing(false);
return true;
}
@Override
public boolean onZoom(ZoomEvent event) {
requestStopsToShow();
return true;
}
}));
btCenterMap.setOnClickListener(v -> {
//Log.i(TAG, "centerMap clicked ");
if(Permissions.locationPermissionGranted(getContext())) {
final GeoPoint myPosition = mLocationOverlay.getMyLocation();
map.getController().animateTo(myPosition);
} else
Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show();
});
btFollowMe.setOnClickListener(v -> {
//Log.i(TAG, "btFollowMe clicked ");
if(Permissions.locationPermissionGranted(getContext()))
setLocationFollowing(!followingLocation);
else
Toast.makeText(getContext(), R.string.enable_position_message_map, Toast.LENGTH_SHORT)
.show();
});
return root;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof FragmentListenerMain) {
listenerMain = (FragmentListenerMain) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement FragmentListenerMain");
}
}
@Override
public void onDetach() {
super.onDetach();
listenerMain = null;
// setupOnAttached = true;
Log.w(DEBUG_TAG, "Fragment detached");
}
@Override
public void onPause() {
super.onPause();
saveMapState();
//cancel asynctask
Log.w(DEBUG_TAG, "On pause called");
if (stopFetcher!= null)
stopFetcher.cancel(true);
}
/**
* Save the map state inside the fragment
* (calls saveMapState(bundle))
*/
private void saveMapState(){
savedMapState = new Bundle();
saveMapState(savedMapState);
}
/**
* Save the state of the map to restore it to a later time
* @param bundle the bundle in which to save the data
*/
private void saveMapState(Bundle bundle){
final IGeoPoint loc = map.getMapCenter();
bundle.putDouble(MAP_CENTER_LAT_KEY, loc.getLatitude());
bundle.putDouble(MAP_CENTER_LON_KEY, loc.getLongitude());
bundle.putDouble(MAP_CURRENT_ZOOM_KEY, map.getZoomLevelDouble());
Log.d(DEBUG_TAG, "Saving state, location following: "+followingLocation);
bundle.putBoolean(FOLLOWING_LOCAT_KEY, followingLocation);
}
@Override
public void onResume() {
super.onResume();
if(listenerMain!=null) listenerMain.readyGUIfor(FragmentKind.MAP);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
saveMapState(outState);
super.onSaveInstanceState(outState);
}
//own methods
/**
* Switch following the location on and off
* @param value true if we want to follow location
*/
public void setLocationFollowing(Boolean value){
followingLocation = value;
if(mLocationOverlay==null || getContext() == null || map ==null)
//nothing else to do
return;
if (value){
mLocationOverlay.enableFollowLocation();
} else {
mLocationOverlay.disableFollowLocation();
}
}
/**
* Do all the stuff you need to do on the gui, when parameter is changed to value
* @param following value
*/
protected void updateGUIForLocationFollowing(boolean following){
if (following)
btFollowMe.setImageResource(R.drawable.ic_follow_me_on);
else
btFollowMe.setImageResource(R.drawable.ic_follow_me);
}
/**
* Build the location overlay. Enable only when
* a) we know we have the permission
* b) the location map is set
*/
private void startLocationOverlay(boolean enableLocation, MapView map){
if(getActivity()== null) throw new IllegalStateException("Cannot enable LocationOverlay now");
// Location Overlay
// from OpenBikeSharing (THANK GOD)
Log.d(DEBUG_TAG, "Starting position overlay");
GpsMyLocationProvider imlp = new GpsMyLocationProvider(getActivity().getBaseContext());
imlp.setLocationUpdateMinDistance(5);
imlp.setLocationUpdateMinTime(2000);
final LocationOverlay overlay = new LocationOverlay(imlp,map, locationCallbacks);
if (enableLocation) overlay.enableMyLocation();
overlay.setOptionsMenuEnabled(true);
//map.getOverlays().add(this.mLocationOverlay);
this.mLocationOverlay = overlay;
map.getOverlays().add(mLocationOverlay);
}
public void startMap(Bundle incoming, Bundle savedInstanceState) {
//Check that we're attached
GeneralActivity activity = getActivity() instanceof GeneralActivity ? (GeneralActivity) getActivity() : null;
if(getContext()==null|| activity==null){
//we are not attached
Log.e(DEBUG_TAG, "Calling startMap when not attached");
return;
}else{
Log.d(DEBUG_TAG, "Starting map from scratch");
}
//clear previous overlays
map.getOverlays().clear();
//parse incoming bundle
GeoPoint marker = null;
String name = null;
String ID = null;
if (incoming != null) {
double lat = incoming.getDouble(BUNDLE_LATIT);
double lon = incoming.getDouble(BUNDLE_LONGIT);
marker = new GeoPoint(lat, lon);
name = incoming.getString(BUNDLE_NAME);
ID = incoming.getString(BUNDLE_ID);
}
//ask for location permission
if(!Permissions.locationPermissionGranted(activity)){
if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){
//TODO: show dialog for permission rationale
Toast.makeText(activity, R.string.enable_position_message_map, Toast.LENGTH_SHORT).show();
}
positionRequestLauncher.launch(Permissions.LOCATION_PERMISSIONS);
}
shownStops = new HashSet<>();
// move the map on the marker position or on a default view point: Turin, Piazza Castello
// and set the start zoom
IMapController mapController = map.getController();
GeoPoint startPoint = null;
startLocationOverlay(Permissions.locationPermissionGranted(activity),
map);
// set the center point
if (marker != null) {
//startPoint = marker;
mapController.setZoom(POSITION_FOUND_ZOOM);
setLocationFollowing(false);
// put the center a little bit off (animate later)
startPoint = new GeoPoint(marker);
startPoint.setLatitude(marker.getLatitude()+ utils.angleRawDifferenceFromMeters(20));
startPoint.setLongitude(marker.getLongitude()-utils.angleRawDifferenceFromMeters(20));
//don't need to do all the rest since we want to show a point
} else if (savedInstanceState != null && savedInstanceState.containsKey(MAP_CURRENT_ZOOM_KEY)) {
mapController.setZoom(savedInstanceState.getDouble(MAP_CURRENT_ZOOM_KEY));
mapController.setCenter(new GeoPoint(savedInstanceState.getDouble(MAP_CENTER_LAT_KEY),
savedInstanceState.getDouble(MAP_CENTER_LON_KEY)));
Log.d(DEBUG_TAG, "Location following from savedInstanceState: "+savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY));
setLocationFollowing(savedInstanceState.getBoolean(FOLLOWING_LOCAT_KEY));
} else {
Log.d(DEBUG_TAG, "No position found from intent or saved state");
boolean found = false;
LocationManager locationManager =
(LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
//check for permission
if (locationManager != null && Permissions.locationPermissionGranted(activity)) {
@SuppressLint("MissingPermission")
Location userLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (userLocation != null) {
mapController.setZoom(POSITION_FOUND_ZOOM);
startPoint = new GeoPoint(userLocation);
found = true;
setLocationFollowing(true);
}
}
if(!found){
startPoint = new GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON);
mapController.setZoom(NO_POSITION_ZOOM);
setLocationFollowing(false);
}
}
// set the minimum zoom level
map.setMinZoomLevel(15.0);
//add contingency check (shouldn't happen..., but)
if (startPoint != null) {
mapController.setCenter(startPoint);
}
//add stops overlay
//map.getOverlays().add(mLocationOverlay);
map.getOverlays().add(this.stopsFolderOverlay);
Log.d(DEBUG_TAG, "Requesting stops load");
// This is not necessary, by setting the center we already move
// the map and we trigger a stop request
//requestStopsToShow();
if (marker != null) {
// make a marker with the info window open for the searched marker
Marker stopMarker = makeMarker(marker, name , ID, true);
map.getController().animateTo(marker);
}
}
/**
* Start a request to load the stops that are in the current view
* from the database
*/
private void requestStopsToShow(){
// get the top, bottom, left and right screen's coordinate
BoundingBox bb = map.getBoundingBox();
double latFrom = bb.getLatSouth();
double latTo = bb.getLatNorth();
double lngFrom = bb.getLonWest();
double lngTo = bb.getLonEast();
if (stopFetcher!= null && stopFetcher.getStatus()!= AsyncTask.Status.FINISHED)
stopFetcher.cancel(true);
stopFetcher = new AsyncStopFetcher(this);
stopFetcher.execute(
new AsyncStopFetcher.BoundingBoxLimit(lngFrom,lngTo,latFrom, latTo));
}
/**
* Add stops as Markers on the map
* @param stops the list of stops that must be included
*/
protected void showStopsMarkers(List stops){
- if (getContext() == null){
+ if (getContext() == null || stops == null){
//we are not attached
return;
}
boolean good = true;
for (Stop stop : stops) {
if (shownStops.contains(stop.ID)){
continue;
}
if(stop.getLongitude()==null || stop.getLatitude()==null)
continue;
shownStops.add(stop.ID);
if(!map.isShown()){
if(good)
Log.d(DEBUG_TAG, "Need to show stop but map is not shown, probably detached already");
good = false;
continue;
} else if(map.getRepository() == null){
Log.e(DEBUG_TAG, "Map view repository is null");
}
GeoPoint marker = new GeoPoint(stop.getLatitude(), stop.getLongitude());
Marker stopMarker = makeMarker(marker, stop.getStopDefaultName(), stop.ID, false);
stopsFolderOverlay.add(stopMarker);
if (!map.getOverlays().contains(stopsFolderOverlay)) {
Log.w(DEBUG_TAG, "Map doesn't have folder overlay");
}
good=true;
}
//Log.d(DEBUG_TAG,"We have " +stopsFolderOverlay.getItems().size()+" stops in the folderOverlay");
//force redraw of markers
map.invalidate();
}
public Marker makeMarker(GeoPoint geoPoint, String stopName, String ID, boolean isStartMarker) {
// add a marker
final Marker marker = new Marker(map);
// set custom info window as info window
CustomInfoWindow popup = new CustomInfoWindow(map, ID, stopName, responder);
marker.setInfoWindow(popup);
// make the marker clickable
marker.setOnMarkerClickListener((thisMarker, mapView) -> {
if (thisMarker.isInfoWindowOpen()) {
// on second click
Log.w(DEBUG_TAG, "Pressed on the click marker");
} else {
// on first click
// hide all opened info window
InfoWindow.closeAllInfoWindowsOn(map);
// show this particular info window
thisMarker.showInfoWindow();
// move the map to its position
map.getController().animateTo(thisMarker.getPosition());
}
return true;
});
// set its position
marker.setPosition(geoPoint);
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
// add to it an icon
//marker.setIcon(getResources().getDrawable(R.drawable.bus_marker));
marker.setIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.bus_marker, ctx.getTheme()));
// add to it a title
marker.setTitle(stopName);
// set the description as the ID
marker.setSnippet(ID);
// show popup info window of the searched marker
if (isStartMarker) {
marker.showInfoWindow();
//map.getController().animateTo(marker.getPosition());
}
return marker;
}
@Nullable
@org.jetbrains.annotations.Nullable
@Override
public View getBaseViewForSnackBar() {
return null;
}
/**
* Simple asyncTask class to load the stops in the background
* Holds a weak reference to the fragment to do callbacks
*/
static class AsyncStopFetcher extends AsyncTask>{
final WeakReference fragmentWeakReference;
public AsyncStopFetcher(MapFragment fragment) {
this.fragmentWeakReference = new WeakReference<>(fragment);
}
@Override
protected List doInBackground(BoundingBoxLimit... limits) {
if(fragmentWeakReference.get()==null || fragmentWeakReference.get().getContext() == null){
Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null");
return null;
}
final BoundingBoxLimit limit = limits[0];
//Log.d(DEBUG_TAG, "Async Stop Fetcher started working");
NextGenDB dbHelper = new NextGenDB(fragmentWeakReference.get().getContext());
- Stop[] stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo,
+ ArrayList stops = dbHelper.queryAllInsideMapView(limit.latitFrom, limit.latitTo,
limit.longFrom, limit.latitTo);
dbHelper.close();
- return Arrays.asList(stops);
+ return stops;
}
@Override
protected void onPostExecute(List stops) {
super.onPostExecute(stops);
//Log.d(DEBUG_TAG, "Async Stop Fetcher has finished working");
if(fragmentWeakReference.get()==null) {
Log.w(DEBUG_TAG, "AsyncLoad fragmentWeakreference null");
return;
}
- Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size());
+ if (stops!=null)
+ Log.d(DEBUG_TAG, "AsyncLoad number of stops: "+stops.size());
fragmentWeakReference.get().showStopsMarkers(stops);
}
private static class BoundingBoxLimit{
final double longFrom, longTo, latitFrom, latitTo;
public BoundingBoxLimit(double longFrom, double longTo, double latitFrom, double latitTo) {
this.longFrom = longFrom;
this.longTo = longTo;
this.latitFrom = latitFrom;
this.latitTo = latitTo;
}
}
}
}