diff --git a/build.gradle b/build.gradle
index e937176..29bc22b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,175 +1,178 @@
buildscript {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
google()
}
ext {
androidXTestVersion = "1.4.0"
//multidex
multidex_version = "2.0.1"
//libraries versions
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.4.1"
arch_version = "2.1.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.2.2'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"
}
}
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 31
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "it.reyboz.bustorino"
minSdkVersion 16
targetSdkVersion 31
versionCode 42
versionName "1.18.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString())
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-ktx:$fragment_version"
implementation "androidx.activity:activity:$activity_version"
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.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-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Legacy
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Room components
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//multidex - we need this to build the app
implementation "androidx.multidex:multidex:$multidex_version"
implementation 'de.siegmar:fastcsv:2.0.0'
testImplementation 'junit:junit:4.12'
implementation 'junit:junit:4.12'
implementation "androidx.test.ext:junit:1.1.3"
implementation "androidx.test:core:$androidXTestVersion"
implementation "androidx.test:runner:$androidXTestVersion"
implementation "androidx.room:room-testing:$room_version"
androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test:core:$androidXTestVersion"
androidTestImplementation "androidx.test:runner:$androidXTestVersion"
androidTestImplementation "androidx.test:rules:$androidXTestVersion"
androidTestImplementation "androidx.room:room-testing:$room_version"
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
}
diff --git a/res/layout/activity_experiments.xml b/res/layout/activity_experiments.xml
index e2fa740..6fca4ea 100644
--- a/res/layout/activity_experiments.xml
+++ b/res/layout/activity_experiments.xml
@@ -1,40 +1,17 @@
-
+
-
-
+ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"/>
\ No newline at end of file
diff --git a/res/layout/activity_map.xml b/res/layout/activity_map.xml
index 1848eeb..7a3229a 100644
--- a/res/layout/activity_map.xml
+++ b/res/layout/activity_map.xml
@@ -1,34 +1,34 @@
\ No newline at end of file
diff --git a/res/layout/fragment_lines_detail.xml b/res/layout/fragment_lines_detail.xml
new file mode 100644
index 0000000..4e1e2f2
--- /dev/null
+++ b/res/layout/fragment_lines_detail.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 04f94cf..9710472 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -1,194 +1,198 @@
Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy.CercaQR Code
+ Si
+ No
+ Installare Barcode Scanner?
+ Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner?Numero fermataNome fermataInserisci il numero della fermataInserisci il nome della fermataVerifica l\'accesso ad Internet!Sembra che nessuna fermata abbia questo nomeNessun passaggio trovato alla fermataErrore di lettura del sito 5T/GTT (dannato sito!)Fermata: %1$sLineaLineeLinea: %1$sLinee: %1$sScegli la fermata…Nessun passaggio
- Nessun QR code
+ Nessun QR code trovato, prova ad usare un\'altra appPreferitiAiutoInformazioniPiù informazioniContribuiscihttps://gitpull.it/w/librebusto/it/Codice sorgenteLicenzaIncontra l\'autoreFermata aggiunta ai preferitiImpossibile aggiungere ai preferiti (memoria piena o database corrotto?)!PreferitiMappaNessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista!RimuoviRinominaRinomina fermataResetInformazioniTocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari:
\n 12:56* Orario in tempo reale\n 12:56 Orario programmato\n\nTrascina giù per aggiornare l\'orario.
\nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo.
OK!
Benvenuto!
Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:
Perché usare BusTO?
- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!
Come Funziona?
Quest\'app ottiene i passaggi dei bus in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".
Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori!
Licenze
L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+.
Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.
Note
Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia.
Buon utilizzo! :)
]]>
Nome troppo corto, digita più caratteri e riprova%1$s verso %2$s%s (destinazione sconosciuta)Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5TVisualizza sulla mappaNon trovo un\'applicazione dove mostrarlaPosizione della fermata non trovataFermate vicineRicerca della posizione in corso…Nessuna fermata nei dintorniPreferenzeAggiornamento del database…Aggiornamento del databaseAggiornamento database forzatoTocca per aggiornare ora il databaseNumero minimo di fermateIl numero di fermate da ricercare non è validoValore errato, inserisci un numeroImpostazioniDistanza massima di ricerca (m)Funzionalità sperimentaliImpostazioniGeneraliFermate recentiImpostazioni generaliGestione del databaseComincia aggiornamento manuale del databaseConsenti l\'accesso alla posizione per mostrarla sulla mappaAbilitare il GPSarriva allealla fermataMostra arriviMostra fermateArrivi qui vicinoFermata rimossa dai preferitiLa mia posizioneSegui posizioneFonte orari: %1$sApp GTTSito GTTSito 5T TorinoApp Muoversi a TorinoSconosciutaCambiamento sorgente orari…Premi a lungo per cambiare la sorgente degli orariCanale unico delle notificheDatabaseInformazioni sul database (aggiornamento)Chiesto troppe volte per il permesso %1$sNon si può usare questa funzionalità senza il permesso di archiviodi archivioUn 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.
L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \nArriviMappaPreferitiApri drawerChiudi drawerEsperimentiOffrici un caffèMappaRicerca fermateVersione appOrari di arrivoRichiesto aggiornamento del databaseMostra direzioni in maiuscoloNon cambiareTutto in maiuscoloSolo la prima lettera maiuscolaMostra arrivi quando tocchi una fermataAbilita esperimentiSchermata da mostrare all\'avvioTocca per cambiareTocca a lungo la fermata per le opzioni
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cd70631..860033a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,219 +1,224 @@
BusTOLibre BusTOBusTO devYou\'re using the latest in technology when it comes to respecting your privacy.
SearchScan QR Code
+ Yes
+ No
+ Install Barcode Scanner?
+ This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now?
+
Bus stop numberBus stop nameInsert bus stop numberInsert bus stop name%1$s towards %2$s%s (unknown destination)Verify your Internet connection!Seems that no bus stop have this nameNo arrivals found for this stopError parsing the 5T/GTT website (damn site!)Name too short, type more characters and retry
Arrivals at: %1$sChoose the bus stop…LineLinesLines: %1$sLine: %1$sNo timetable found
- No QR code
+ No QR code found, try using another app to scanUnexpected internal error, cannot extract data from GTT/5T websiteHelpAboutMore aboutContributehttps://gitpull.it/w/librebusto/en/Source codeLicence11
Meet the authorBus stop is now in your favoritesBus stop removed from your favoritesFavoritesFavoritesMapNo favorites? Arghh! Press on a bus stop star to populate this list!DeleteRenameRename the bus stopResetAboutTap the star
to add the bus stop to the favourites\n\nHow to read timelines:\n 12:56* Real-time
arrivals\n 12:56 Scheduled arrivals\n\nPull down to refresh the timetable
\n Long press on Arrivals source to change the source of the arrival times
GOT IT!Arrival times
Welcome!
Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.
Why use this app?
- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!
How does it work?
This app will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".
Who worked on BusTO:
- Fabio Mazza current senior rockstar developer.
- Andrea Ugo current junior rockstar developer.
- Silviu Chiriac designer of the 2021 logo.
- Marco M rockstar tester and bug hunter.
- Ludovico Pavesi previous senior rockstar developer asd.
- Valerio Bozzolan maintainer and infrastructure sponsor.
- Marco Gagino contributor and icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!
Licenses
The app and the related source code are released by Valerio Bozzolan under the terms of the GNU General Public License v3+).
So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.
Notes
This app has been developed hoping to be useful to everyone but without ANY warranty.
This translation is kindly provided by Riccardo Caniato and Marco Gagino.
Get involved! :)
]]>
Cannot add to favorites (storage full or corrupted database?)!View on a mapCannot find any application to show it inCannot find the position of the stopListFragment - BusTOit.reyboz.bustorino.preferencesdb_is_updatingNearby stopsNearby connectionsApp versionThe number of stops to show in the recents is invalidInvalid value, put a valid numberFinding the position…No stops nearbyMinimum number of stopsPreferencesSettingsSettingsGeneralExperimental featuresMaximum distance (meters)Recent stopsGeneral settingsDatabase managementLaunch manual database updateAllow access to position to show it on the mapPlease enable GPSDatabase update in progress…Updating the databaseForce database updateTouch to update the app database nowis arriving atat the stop%1$s - %2$sShow arrivalsShow stopsCenter on my locationFollow meArrivals source: %1$sGTT AppGTT Website5T Torino websiteMuoversi a Torino appUndeterminedChanging arrival times source…Long press to change the source of arrivalsDefaultDefault channel for notificationsDatabaseNotifications on the update of the databaseAsked for %1$s permission too many timesCannot use the map with the storage permission!storageThe 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.
The application crashed and the crash report is in the attachments. Please
describe what you were doing before the crash: \n
ArrivalsMapFavoritesOpen navigation drawerClose navigation drawerExperimentsBuy us a coffeeMapSearch by stopLaunching database updateCapitalize directionsDo not change arrivals directionsCapitalize everythingCapitalize only first letterKEEPCAPITALIZE_ALLCAPITALIZE_FIRSTSection to show on startupTouch to change itShow arrivals touching on stopEnable experimentsLong press the stop for options@string/nav_arrivals_text@string/nav_favorites_text@string/nav_map_text@string/lines
diff --git a/src/com/google/zxing/integration/android/IntentIntegrator.java b/src/com/google/zxing/integration/android/IntentIntegrator.java
index 6660cd8..d3bc081 100644
--- a/src/com/google/zxing/integration/android/IntentIntegrator.java
+++ b/src/com/google/zxing/integration/android/IntentIntegrator.java
@@ -1,506 +1,507 @@
/*
* Copyright 2009 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.zxing.integration.android;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
/**
*
A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple
* way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the
* project's source code.
*
*
Initiating a barcode scan
*
*
To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait
* for the result in your app.
*
*
It does require that the Barcode Scanner (or work-alike) application is installed. The
* {@link #initiateScan()} method will prompt the user to download the application, if needed.
*
*
There are a few steps to using this integration. First, your {@link Activity} must implement
* the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:
*
*
{@code
* public void onActivityResult(int requestCode, int resultCode, Intent intent) {
* IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
* if (scanResult != null) {
* // handle scan result
* }
* // else continue with any other code you need in the method
* ...
* }
* }
*
*
This is where you will handle a scan result.
*
*
Second, just call this in response to a user action somewhere to begin the scan process:
Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the
* user was prompted to download the application. This lets the calling app potentially manage the dialog.
* In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()}
* method.
*
*
You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use
* {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and
* yes/no button labels can be changed.
*
*
Finally, you can use {@link #addExtra(String, Object)} to add more parameters to the Intent used
* to invoke the scanner. This can be used to set additional options not directly exposed by this
* simplified API.
*
*
By default, this will only allow applications that are known to respond to this intent correctly
* do so. The apps that are allowed to response can be set with {@link #setTargetApplications(List)}.
* For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.
*
*
Sharing text via barcode
*
*
To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.
*
*
Some code, particularly download integration, was contributed from the Anobiit application.
*
*
Enabling experimental barcode formats
*
*
Some formats are not enabled by default even when scanning with {@link #ALL_CODE_TYPES}, such as
* PDF417. Use {@link #initiateScan(java.util.Collection)} with
* a collection containing the names of formats to scan for explicitly, like "PDF_417", to use such
* formats.
*
* @author Sean Owen
* @author Fred Lin
* @author Isaac Potoczny-Jones
* @author Brad Drehmer
* @author gcstang
*/
public class IntentIntegrator {
public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits
private static final String TAG = IntentIntegrator.class.getSimpleName();
public static final String DEFAULT_TITLE = "Install Barcode Scanner?";
public static final String DEFAULT_MESSAGE =
"This application requires Barcode Scanner. Would you like to install it?";
public static final String DEFAULT_YES = "Yes";
public static final String DEFAULT_NO = "No";
private static final String BS_PACKAGE = "com.google.zxing.client.android";
private static final String BSPLUS_PACKAGE = "com.srowen.bs.android";
// supported barcode formats
public static final Collection PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14");
public static final Collection ONE_D_CODE_TYPES =
list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128",
"ITF", "RSS_14", "RSS_EXPANDED");
public static final Collection QR_CODE_TYPES = Collections.singleton("QR_CODE");
public static final Collection DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX");
public static final Collection ALL_CODE_TYPES = null;
public static final List TARGET_BARCODE_SCANNER_ONLY = Collections.singletonList(BS_PACKAGE);
public static final List TARGET_ALL_KNOWN = list(
BSPLUS_PACKAGE, // Barcode Scanner+
BSPLUS_PACKAGE + ".simple", // Barcode Scanner+ Simple
BS_PACKAGE // Barcode Scanner
// What else supports this intent?
);
private final Activity activity;
private final Fragment fragment;
private String title;
private String message;
private String buttonYes;
private String buttonNo;
private List targetApplications;
private final Map moreExtras = new HashMap(3);
/**
* @param activity {@link Activity} invoking the integration
*/
public IntentIntegrator(Activity activity) {
this.activity = activity;
this.fragment = null;
initializeConfiguration();
}
/**
* @param fragment {@link Fragment} invoking the integration.
* {@link #startActivityForResult(Intent, int)} will be called on the {@link Fragment} instead
* of an {@link Activity}
*/
public IntentIntegrator(Fragment fragment) {
this.activity = fragment.getActivity();
this.fragment = fragment;
initializeConfiguration();
}
private void initializeConfiguration() {
title = DEFAULT_TITLE;
message = DEFAULT_MESSAGE;
buttonYes = DEFAULT_YES;
buttonNo = DEFAULT_NO;
targetApplications = TARGET_ALL_KNOWN;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public void setTitleByID(int titleID) {
title = activity.getString(titleID);
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public void setMessageByID(int messageID) {
message = activity.getString(messageID);
}
public String getButtonYes() {
return buttonYes;
}
public void setButtonYes(String buttonYes) {
this.buttonYes = buttonYes;
}
public void setButtonYesByID(int buttonYesID) {
buttonYes = activity.getString(buttonYesID);
}
public String getButtonNo() {
return buttonNo;
}
public void setButtonNo(String buttonNo) {
this.buttonNo = buttonNo;
}
public void setButtonNoByID(int buttonNoID) {
buttonNo = activity.getString(buttonNoID);
}
public Collection getTargetApplications() {
return targetApplications;
}
public final void setTargetApplications(List targetApplications) {
if (targetApplications.isEmpty()) {
throw new IllegalArgumentException("No target applications");
}
this.targetApplications = targetApplications;
}
public void setSingleTargetApplication(String targetApplication) {
this.targetApplications = Collections.singletonList(targetApplication);
}
public Map getMoreExtras() {
return moreExtras;
}
public final void addExtra(String key, Object value) {
moreExtras.put(key, value);
}
/**
* Initiates a scan for all known barcode types with the default camera.
*
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan() {
return initiateScan(ALL_CODE_TYPES, -1);
}
/**
* Initiates a scan for all known barcode types with the specified camera.
*
* @param cameraId camera ID of the camera to use. A negative value means "no preference".
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan(int cameraId) {
return initiateScan(ALL_CODE_TYPES, cameraId);
}
/**
* Initiates a scan, using the default camera, only for a certain set of barcode types, given as strings corresponding
* to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
* like {@link #PRODUCT_CODE_TYPES} for example.
*
* @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise.
*/
public final AlertDialog initiateScan(Collection desiredBarcodeFormats) {
return initiateScan(desiredBarcodeFormats, -1);
}
/**
* Initiates a scan, using the specified camera, only for a certain set of barcode types, given as strings corresponding
* to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants
* like {@link #PRODUCT_CODE_TYPES} for example.
*
* @param desiredBarcodeFormats names of {@code BarcodeFormat}s to scan for
* @param cameraId camera ID of the camera to use. A negative value means "no preference".
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
*/
public final AlertDialog initiateScan(Collection desiredBarcodeFormats, int cameraId) {
Intent intentScan = new Intent(BS_PACKAGE + ".SCAN");
intentScan.addCategory(Intent.CATEGORY_DEFAULT);
// check which types of codes to scan for
if (desiredBarcodeFormats != null) {
// set the desired barcode types
StringBuilder joinedByComma = new StringBuilder();
for (String format : desiredBarcodeFormats) {
if (joinedByComma.length() > 0) {
joinedByComma.append(',');
}
joinedByComma.append(format);
}
intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString());
}
// check requested camera ID
if (cameraId >= 0) {
intentScan.putExtra("SCAN_CAMERA_ID", cameraId);
}
String targetAppPackage = findTargetAppPackage(intentScan);
if (targetAppPackage == null) {
return showDownloadDialog();
}
intentScan.setPackage(targetAppPackage);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
attachMoreExtras(intentScan);
startActivityForResult(intentScan, REQUEST_CODE);
return null;
}
/**
* Start an activity. This method is defined to allow different methods of activity starting for
* newer versions of Android and for compatibility library.
*
* @param intent Intent to start.
* @param code Request code for the activity
* @see android.app.Activity#startActivityForResult(Intent, int)
* @see android.app.Fragment#startActivityForResult(Intent, int)
*/
protected void startActivityForResult(Intent intent, int code) {
if (fragment == null) {
activity.startActivityForResult(intent, code);
} else {
fragment.startActivityForResult(intent, code);
}
}
private String findTargetAppPackage(Intent intent) {
PackageManager pm = activity.getPackageManager();
List availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (availableApps != null) {
+ Log.d("IntentIntegrator","Available app to scan QR Code: "+availableApps);
for (String targetApp : targetApplications) {
if (contains(availableApps, targetApp)) {
return targetApp;
}
}
}
return null;
}
private static boolean contains(Iterable availableApps, String targetApp) {
for (ResolveInfo availableApp : availableApps) {
String packageName = availableApp.activityInfo.packageName;
if (targetApp.equals(packageName)) {
return true;
}
}
return false;
}
private AlertDialog showDownloadDialog() {
AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity);
downloadDialog.setTitle(title);
downloadDialog.setMessage(message);
downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
String packageName;
if (targetApplications.contains(BS_PACKAGE)) {
// Prefer to suggest download of BS if it's anywhere in the list
packageName = BS_PACKAGE;
} else {
// Otherwise, first option:
packageName = targetApplications.get(0);
}
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
if (fragment == null) {
activity.startActivity(intent);
} else {
fragment.startActivity(intent);
}
} catch (ActivityNotFoundException anfe) {
// Hmm, market is not installed
Log.w(TAG, "Google Play is not installed; cannot install " + packageName);
}
}
});
downloadDialog.setNegativeButton(buttonNo, null);
downloadDialog.setCancelable(true);
return downloadDialog.show();
}
/**
*
Call this from your {@link Activity}'s
* {@link Activity#onActivityResult(int, int, Intent)} method.
*
* @param requestCode request code from {@code onActivityResult()}
* @param resultCode result code from {@code onActivityResult()}
* @param intent {@link Intent} from {@code onActivityResult()}
* @return null if the event handled here was not related to this class, or
* else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning,
* the fields will be null.
*/
public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
String contents = intent.getStringExtra("SCAN_RESULT");
String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT");
byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES");
int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE);
Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation;
String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL");
return new IntentResult(contents,
formatName,
rawBytes,
orientation,
errorCorrectionLevel);
}
return new IntentResult();
}
return null;
}
/**
* Defaults to type "TEXT_TYPE".
*
* @param text the text string to encode as a barcode
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
* @see #shareText(CharSequence, CharSequence)
*/
public final AlertDialog shareText(CharSequence text) {
return shareText(text, "TEXT_TYPE");
}
/**
* Shares the given text by encoding it as a barcode, such that another user can
* scan the text off the screen of the device.
*
* @param text the text string to encode as a barcode
* @param type type of data to encode. See {@code com.google.zxing.client.android.Contents.Type} constants.
* @return the {@link AlertDialog} that was shown to the user prompting them to download the app
* if a prompt was needed, or null otherwise
*/
public final AlertDialog shareText(CharSequence text, CharSequence type) {
Intent intent = new Intent();
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setAction(BS_PACKAGE + ".ENCODE");
intent.putExtra("ENCODE_TYPE", type);
intent.putExtra("ENCODE_DATA", text);
String targetAppPackage = findTargetAppPackage(intent);
if (targetAppPackage == null) {
return showDownloadDialog();
}
intent.setPackage(targetAppPackage);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
attachMoreExtras(intent);
if (fragment == null) {
activity.startActivity(intent);
} else {
fragment.startActivity(intent);
}
return null;
}
private static List list(String... values) {
return Collections.unmodifiableList(Arrays.asList(values));
}
private void attachMoreExtras(Intent intent) {
for (Map.Entry entry : moreExtras.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Kind of hacky
if (value instanceof Integer) {
intent.putExtra(key, (Integer) value);
} else if (value instanceof Long) {
intent.putExtra(key, (Long) value);
} else if (value instanceof Boolean) {
intent.putExtra(key, (Boolean) value);
} else if (value instanceof Double) {
intent.putExtra(key, (Double) value);
} else if (value instanceof Float) {
intent.putExtra(key, (Float) value);
} else if (value instanceof Bundle) {
intent.putExtra(key, (Bundle) value);
} else {
intent.putExtra(key, value.toString());
}
}
}
}
diff --git a/src/it/reyboz/bustorino/ActivityExperiments.java b/src/it/reyboz/bustorino/ActivityExperiments.java
index 3fb069c..6427b89 100644
--- a/src/it/reyboz/bustorino/ActivityExperiments.java
+++ b/src/it/reyboz/bustorino/ActivityExperiments.java
@@ -1,155 +1,40 @@
/*
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;
-import android.content.Context;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.Toast;
import android.os.Bundle;
-import it.reyboz.bustorino.backend.Fetcher;
-import it.reyboz.bustorino.backend.gtfs.GtfsDataParser;
-import it.reyboz.bustorino.backend.networkTools;
-import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
-import it.reyboz.bustorino.data.gtfs.GtfsDBDao;
+import it.reyboz.bustorino.fragments.LinesDetailFragment;
import it.reyboz.bustorino.middleware.GeneralActivity;
-import java.io.*;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.*;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
public class ActivityExperiments extends GeneralActivity {
- ExecutorService executorService;
final static String DEBUG_TAG = "ExperimentsGTFS";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_experiments);
- Button deleteButton = findViewById(R.id.deleteButton);
- if(deleteButton!=null)
- deleteButton.setOnClickListener(view -> {
- File saveFile = new File(getFilesDir(), "gtfs_data.zip");
- if(!saveFile.isDirectory() && saveFile.exists()){
- //delete the file
- if(saveFile.delete())
- Toast.makeText(this, "Gtfs zip deleted", Toast.LENGTH_SHORT).show();
- else
- Toast.makeText(this, "Cannot delete gtfs zip", Toast.LENGTH_SHORT).show();
- } else
- Toast.makeText(this, "Gtfs data zip not present", Toast.LENGTH_SHORT).show();
- });
-
- Button cleanDBButton = findViewById(R.id.deleteDbButton);
- if(cleanDBButton!=null)
- cleanDBButton.setOnClickListener(this::deleteDatabase);
-
- executorService = Executors.newFixedThreadPool(2);
- }
-
- public void runExp(View v){
-
- final Context appContext = v.getContext().getApplicationContext();
-
- Runnable run = new Runnable() {
- @Override
- public void run() {
-
- AtomicReference res = new AtomicReference<>();
- //List files = GtfsDataParser.readFilesList(res);
- Date updateDate = GtfsDataParser.getLastGTFSUpdateDate(res);
- Log.w(
- "ExperimentGTFS", "Last update date is " + updateDate//utils.joinList(files, "\n")
- );
- //Toast.makeText(v.getContext(), "Gtfs data already downloaded", Toast.LENGTH_SHORT).show();
- GtfsDBDao dao = GtfsDatabase.Companion.getGtfsDatabase(appContext).gtfsDao();
- Log.d(DEBUG_TAG, String.valueOf(dao));
- dao.deleteAllStopTimes();
-
-
- File saveFile = new File(getFilesDir(), "gtfs_data.zip");
- if (!saveFile.isDirectory() && saveFile.exists()) {
- Log.w(DEBUG_TAG, "Zip exists: " + saveFile);
-
-
- try (ZipFile zipFile = new ZipFile(saveFile)) {
- //ZipInputStream stream = new ZipInputStream(fileStream);
- // now iterate through each item in the stream. The get next
- // entry call will return a ZipEntry for each file in the
- // stream
- /*
- Enumeration extends ZipEntry> entries = zipFile.entries();
- ZipEntry entry;
- String line;
- //final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
-
- HashSet readLater = new HashSet<>();
- while(entries.hasMoreElements()){
- entry = entries.nextElement();
- //String tableName = entry.getName().split("\\.")[0].trim();
- if(entry.getName().trim().equals("stop_times.txt")) {
- readLater.add(entry);
- continue;
- }
- GtfsDataParser.readGtfsZipEntry(entry, zipFile, v.getContext().getApplicationContext());
- }
- for(ZipEntry laterEntry: readLater){
- GtfsDataParser.readGtfsZipEntry(laterEntry, zipFile, v.getContext().getApplicationContext());
- }
-
- */
- } catch (IOException e) {
- e.printStackTrace();
- }
- //saveFile.delete();
- } else
- try {
- //Toast.makeText(v.getContext(), "Downloading gtfs data", Toast.LENGTH_SHORT).show();
-
- networkTools.saveFileInCache(saveFile, new URL(GtfsDataParser.GTFS_ADDRESS));
- Log.w(DEBUG_TAG, "File saved");
- } catch (MalformedURLException e) {
- e.printStackTrace();
- }
-
- }
- };
-
- Toast.makeText(this, "Test disabled", Toast.LENGTH_SHORT).show();
- //Looper looper = new Looper(true);
- //Handler handler = new Handler();
- //handler.post(run);
- //executorService.execute(run);
-
-
- }
-
- public void deleteDatabase(View v){
- //final Context con = getApplicationContext().getApplicationContext();
- Toast.makeText(this, "Deleting GTFS DB contents isn't allowed anymore", Toast.LENGTH_SHORT).show();
-
+ if (savedInstanceState==null) {
+ getSupportFragmentManager().beginTransaction()
+ .setReorderingAllowed(true)
+ .add(R.id.fragment_container_view, LinesDetailFragment.class,
+ LinesDetailFragment.Companion.makeArgs("gtt:56U"))
+ .commit();
+ }
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java
index c960e73..c034a8f 100644
--- a/src/it/reyboz/bustorino/ActivityPrincipal.java
+++ b/src/it/reyboz/bustorino/ActivityPrincipal.java
@@ -1,668 +1,695 @@
/*
BusTO - Arrival times for Turin public transport.
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;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import java.util.Arrays;
import it.reyboz.bustorino.backend.Stop;
import it.reyboz.bustorino.data.DBUpdateWorker;
import it.reyboz.bustorino.data.DatabaseUpdate;
import it.reyboz.bustorino.data.PreferencesHolder;
import it.reyboz.bustorino.data.gtfs.GtfsDatabase;
import it.reyboz.bustorino.fragments.*;
import it.reyboz.bustorino.middleware.GeneralActivity;
import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.backend.utils.openIceweasel;
public class ActivityPrincipal extends GeneralActivity implements FragmentListenerMain {
private DrawerLayout mDrawer;
private NavigationView mNavView;
private ActionBarDrawerToggle drawerToggle;
private final static String DEBUG_TAG="BusTO Act Principal";
private final static String TAG_FAVORITES="favorites_frag";
private Snackbar snackbar;
private boolean showingMainFragmentFromOther = false;
+ private boolean onCreateComplete = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_principal);
final SharedPreferences theShPr = getMainSharedPreferences();
- boolean showingArrivalsForStop = false;
+ boolean showingArrivalsFromIntent = false;
//database check
GtfsDatabase gtfsDB = GtfsDatabase.Companion.getGtfsDatabase(this);
final int db_version = gtfsDB.getOpenHelper().getReadableDatabase().getVersion();
boolean dataUpdateRequested = false;
final int old_version = PreferencesHolder.getGtfsDBVersion(theShPr);
Log.d(DEBUG_TAG, "GTFS Database: old version is "+old_version+ ", new version is "+db_version);
if (old_version < db_version){
//decide update conditions in the future
if(old_version < 2 && db_version >= 2) {
dataUpdateRequested = true;
DatabaseUpdate.requestDBUpdateWithWork(this, true, true);
}
PreferencesHolder.setGtfsDBVersion(theShPr, db_version);
}
Toolbar mToolbar = findViewById(R.id.default_toolbar);
setSupportActionBar(mToolbar);
if (getSupportActionBar()!=null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
else Log.w(DEBUG_TAG, "NO ACTION BAR");
mToolbar.setOnMenuItemClickListener(new ToolbarItemClickListener(this));
mDrawer = findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle(mToolbar);
// Setup toggle to display hamburger icon with nice animation
drawerToggle.setDrawerIndicatorEnabled(true);
drawerToggle.syncState();
mDrawer.addDrawerListener(drawerToggle);
mDrawer.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
}
@Override
public void onDrawerOpened(@NonNull View drawerView) {
hideKeyboard();
}
@Override
public void onDrawerClosed(@NonNull View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
});
mNavView = findViewById(R.id.nvView);
setupDrawerContent(mNavView);
/*View header = mNavView.getHeaderView(0);
*/
//mNavView.getMenu().findItem(R.id.versionFooter).
/// LEGACY CODE
//---------------------------- START INTENT CHECK QUEUE ------------------------------------
// Intercept calls from URL intent
boolean tryedFromIntent = false;
String busStopID = null;
Uri data = getIntent().getData();
if (data != null) {
+
busStopID = getBusStopIDFromUri(data);
+ Log.d(DEBUG_TAG, "Opening Intent: busStopID: "+busStopID);
tryedFromIntent = true;
}
// Intercept calls from other activities
if (!tryedFromIntent) {
Bundle b = getIntent().getExtras();
if (b != null) {
busStopID = b.getString("bus-stop-ID");
/*
* I'm not very sure if you are coming from an Intent.
* Some launchers work in strange ways.
*/
tryedFromIntent = busStopID != null;
}
}
//---------------------------- END INTENT CHECK QUEUE --------------------------------------
if (busStopID == null) {
// Show keyboard if can't start from intent
// JUST DON'T
// showKeyboard();
// You haven't obtained anything... from an intent?
if (tryedFromIntent) {
// This shows a luser warning
Toast.makeText(getApplicationContext(),
R.string.insert_bus_stop_number_error, Toast.LENGTH_SHORT).show();
}
} else {
// If you are here an intent has worked successfully
//setBusStopSearchByIDEditText(busStopID);
-
- requestArrivalsForStopID(busStopID);
- showingArrivalsForStop = true;
+ //Log.d(DEBUG_TAG, "Requesting arrivals for stop "+busStopID+" from intent");
+ requestArrivalsForStopID(busStopID); //this shows the fragment, too
+ showingArrivalsFromIntent = true;
}
//Try (hopefully) database update
if(!dataUpdateRequested)
DatabaseUpdate.requestDBUpdateWithWork(this, false, false);
/*
Watch for database update
*/
final WorkManager workManager = WorkManager.getInstance(this);
workManager.getWorkInfosForUniqueWorkLiveData(DBUpdateWorker.DEBUG_TAG)
.observe(this, workInfoList -> {
// If there are no matching work info, do nothing
if (workInfoList == null || workInfoList.isEmpty()) {
return;
}
Log.d(DEBUG_TAG, "WorkerInfo: "+workInfoList);
boolean showProgress = false;
for (WorkInfo workInfo : workInfoList) {
if (workInfo.getState() == WorkInfo.State.RUNNING) {
showProgress = true;
break;
}
}
if (showProgress) {
createDefaultSnackbar();
} else {
if(snackbar!=null) {
snackbar.dismiss();
snackbar = null;
}
}
});
// show the main fragment
Fragment f = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
Log.d(DEBUG_TAG, "OnCreate the fragment is "+f);
String vl = PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.PREF_KEY_STARTUP_SCREEN, "");
//if (vl.length() == 0 || vl.equals("arrivals")) {
// showMainFragment();
Log.d(DEBUG_TAG, "The default screen to open is: "+vl);
- if(showingArrivalsForStop){
- showMainFragment(false);
- } else if (vl.equals("map")){
+ if (showingArrivalsFromIntent){
+ //do nothing but exclude a case
+ }
+ else if (vl.equals("map")){
requestMapFragment(false);
} else if(vl.equals("favorites")){
checkAndShowFavoritesFragment(getSupportFragmentManager(), false);
} else if(vl.equals("lines")){
showLinesFragment(getSupportFragmentManager(), false, null);
- } else{
+ } else {
showMainFragment(false);
}
-
+ onCreateComplete = true;
}
private ActionBarDrawerToggle setupDrawerToggle(Toolbar toolbar) {
// NOTE: Make sure you pass in a valid toolbar reference. ActionBarDrawToggle() does not require it
// and will not render the hamburger icon without it.
return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}
/**
* Setup drawer actions
* @param navigationView the navigation view on which to set the callbacks
*/
private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
menuItem -> {
if (menuItem.getItemId() == R.id.drawer_action_settings) {
Log.d("MAINBusTO", "Pressed button preferences");
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivitySettings.class));
return true;
} else if(menuItem.getItemId() == R.id.nav_favorites_item){
closeDrawerIfOpen();
//get Fragment
checkAndShowFavoritesFragment(getSupportFragmentManager(), true);
return true;
} else if(menuItem.getItemId() == R.id.nav_arrivals){
closeDrawerIfOpen();
showMainFragment(true);
return true;
} else if(menuItem.getItemId() == R.id.nav_map_item){
closeDrawerIfOpen();
requestMapFragment(true);
return true;
} else if (menuItem.getItemId() == R.id.nav_lines_item) {
closeDrawerIfOpen();
showLinesFragment(getSupportFragmentManager(), true,null);
return true;
} else if(menuItem.getItemId() == R.id.drawer_action_info) {
closeDrawerIfOpen();
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
}
//selectDrawerItem(menuItem);
Log.d(DEBUG_TAG, "pressed item "+menuItem);
return true;
});
}
private void closeDrawerIfOpen(){
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
}
// `onPostCreate` called when activity start-up is complete after `onStart()`
// NOTE 1: Make sure to override the method with only a single `Bundle` argument
// Note 2: Make sure you implement the correct `onPostCreate(Bundle savedInstanceState)` method.
// There are 2 signatures and only `onPostCreate(Bundle state)` shows the hamburger icon.
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Sync the toggle state after onRestoreInstanceState has occurred.
drawerToggle.syncState();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Pass any configuration change to the drawer toggles
drawerToggle.onConfigurationChanged(newConfig);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.principal_menu, menu);
MenuItem experimentsMenuItem = menu.findItem(R.id.action_experiments);
SharedPreferences shPr = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean exper_On = shPr.getBoolean(getString(R.string.pref_key_experimental), false);
experimentsMenuItem.setVisible(exper_On);
return super.onCreateOptionsMenu(menu);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode==STORAGE_PERMISSION_REQ){
final String storagePerm = Manifest.permission.WRITE_EXTERNAL_STORAGE;
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(DEBUG_TAG, "Permissions check: " + Arrays.toString(permissions));
if (permissionDoneRunnables.containsKey(storagePerm)) {
Runnable toRun = permissionDoneRunnables.get(storagePerm);
if (toRun != null)
toRun.run();
permissionDoneRunnables.remove(storagePerm);
}
} else {
//permission denied
showToastMessage(R.string.permission_storage_maps_msg, false);
}
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int[] cases = {R.id.nav_arrivals, R.id.nav_favorites_item};
Log.d(DEBUG_TAG, "Item pressed");
if (item.getItemId() == android.R.id.home) {
mDrawer.openDrawer(GravityCompat.START);
return true;
}
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
boolean foundFragment = false;
Fragment shownFrag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (mDrawer.isDrawerOpen(GravityCompat.START))
mDrawer.closeDrawer(GravityCompat.START);
else if(shownFrag != null && shownFrag.isVisible() && shownFrag.getChildFragmentManager().getBackStackEntryCount() > 0){
//if we have been asked to show a stop from another fragment, we should go back even in the main
if(shownFrag instanceof MainScreenFragment){
//we have to stop the arrivals reload
((MainScreenFragment) shownFrag).cancelReloadArrivalsIfNeeded();
}
shownFrag.getChildFragmentManager().popBackStack();
if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main back stack also");
}
}
else if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack();
Log.d(DEBUG_TAG, "Popping main frame backstack for fragments");
}
else
super.onBackPressed();
}
/**
* Create and show the SnackBar with the message
*/
private void createDefaultSnackbar() {
View baseView = null;
final Fragment frag = getSupportFragmentManager().findFragmentById(R.id.mainActContentFrame);
if (frag instanceof ScreenBaseFragment){
baseView = ((ScreenBaseFragment) frag).getBaseViewForSnackBar();
}
if (baseView == null) baseView = findViewById(R.id.mainActContentFrame);
if (baseView == null) Log.e(DEBUG_TAG, "baseView null for default snackbar, probably exploding now");
snackbar = Snackbar.make(baseView, R.string.database_update_msg_inapp, Snackbar.LENGTH_INDEFINITE);
snackbar.show();
}
/**
* Show the fragment by adding it to the backstack
* @param fraMan the fragmentManager
* @param fragment the fragment
*/
private static void showMainFragment(FragmentManager fraMan, MainScreenFragment fragment, boolean addToBackStack){
FragmentTransaction ft = fraMan.beginTransaction()
.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG)
.setReorderingAllowed(false)
/*.setCustomAnimations(
R.anim.slide_in, // enter
R.anim.fade_out, // exit
R.anim.fade_in, // popEnter
R.anim.slide_out // popExit
)*/
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToBackStack) ft.addToBackStack(null);
ft.commit();
}
+ /**
+ * Show the fragment by adding it to the backstack
+ * @param fraMan the fragmentManager
+ * @param arguments args for the fragment
+ */
+ private static void createShowMainFragment(FragmentManager fraMan, Bundle arguments, boolean addToBackStack){
+ FragmentTransaction ft = fraMan.beginTransaction()
+ .replace(R.id.mainActContentFrame, MainScreenFragment.class, arguments, MainScreenFragment.FRAGMENT_TAG)
+ .setReorderingAllowed(false)
+ /*.setCustomAnimations(
+ R.anim.slide_in, // enter
+ R.anim.fade_out, // exit
+ R.anim.fade_in, // popEnter
+ R.anim.slide_out // popExit
+ )*/
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ if (addToBackStack) ft.addToBackStack(null);
+ ft.commit();
+ }
private void requestMapFragment(final boolean allowReturn){
final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE;
int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ);
switch (result) {
case PERMISSION_OK:
createAndShowMapFragment(null, allowReturn);
break;
case PERMISSION_ASKING:
permissionDoneRunnables.put(permission,
() -> createAndShowMapFragment(null, allowReturn));
break;
case PERMISSION_NEG_CANNOT_ASK:
String storage_perm = getString(R.string.storage_permission);
String text = getString(R.string.too_many_permission_asks, storage_perm);
Toast.makeText(getApplicationContext(),text, Toast.LENGTH_LONG).show();
}
}
private static void checkAndShowFavoritesFragment(FragmentManager fragmentManager, boolean addToBackStack){
FragmentTransaction ft = fragmentManager.beginTransaction();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_FAVORITES);
if(fragment!=null){
ft.replace(R.id.mainActContentFrame, fragment, TAG_FAVORITES);
}else{
//use new method
ft.replace(R.id.mainActContentFrame,FavoritesFragment.class,null,TAG_FAVORITES);
}
if (addToBackStack)
ft.addToBackStack("favorites_main");
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.setReorderingAllowed(false);
ft.commit();
}
private static void showLinesFragment(@NonNull FragmentManager fragmentManager, boolean addToBackStack, @Nullable Bundle fragArgs){
FragmentTransaction ft = fragmentManager.beginTransaction();
Fragment f = fragmentManager.findFragmentByTag(LinesFragment.FRAGMENT_TAG);
if(f!=null){
ft.replace(R.id.mainActContentFrame, f, LinesFragment.FRAGMENT_TAG);
}else{
//use new method
ft.replace(R.id.mainActContentFrame,LinesFragment.class,fragArgs,LinesFragment.FRAGMENT_TAG);
}
if (addToBackStack)
ft.addToBackStack("lines");
ft.setReorderingAllowed(true)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
}
private MainScreenFragment showMainFragment(boolean addToBackStack){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
final MainScreenFragment mainScreenFragment;
if (fragment==null | !(fragment instanceof MainScreenFragment)){
mainScreenFragment = MainScreenFragment.newInstance();
//mainScreenFragment = createAndShowMainFragment();
showMainFragment(fraMan, mainScreenFragment, addToBackStack);
}
else if(!fragment.isVisible()){
mainScreenFragment = (MainScreenFragment) fragment;
showMainFragment(fraMan, mainScreenFragment, addToBackStack);
Log.d(DEBUG_TAG, "Found the main fragment");
} else{
mainScreenFragment = (MainScreenFragment) fragment;
}
return mainScreenFragment;
}
@Nullable
private MainScreenFragment getMainFragmentIfVisible(){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
if (fragment!= null && fragment.isVisible()) return (MainScreenFragment) fragment;
else return null;
}
@Override
public void showFloatingActionButton(boolean yes) {
//TODO
}
/*
public void setDrawerSelectedItem(String fragmentTag){
switch (fragmentTag){
case MainScreenFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MapFragment.FRAGMENT_TAG:
break;
case FavoritesFragment.FRAGMENT_TAG:
mNavView.setCheckedItem(R.id.nav_favorites_item);
break;
}
}*/
@Override
public void readyGUIfor(FragmentKind fragmentType) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.readyGUIfor(fragmentType);
}
int titleResId;
switch (fragmentType){
case MAP:
mNavView.setCheckedItem(R.id.nav_map_item);
titleResId = R.string.map;
break;
case FAVORITES:
mNavView.setCheckedItem(R.id.nav_favorites_item);
titleResId = R.string.nav_favorites_text;
break;
case ARRIVALS:
titleResId = R.string.nav_arrivals_text;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case STOPS:
titleResId = R.string.stop_search_view_title;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case MAIN_SCREEN_FRAGMENT:
case NEARBY_STOPS:
case NEARBY_ARRIVALS:
titleResId=R.string.app_name_full;
mNavView.setCheckedItem(R.id.nav_arrivals);
break;
case LINES:
titleResId=R.string.lines;
mNavView.setCheckedItem(R.id.nav_lines_item);
break;
default:
titleResId = 0;
}
if(getSupportActionBar()!=null && titleResId!=0)
getSupportActionBar().setTitle(titleResId);
}
@Override
public void requestArrivalsForStopID(String ID) {
//register if the request came from the main fragment or not
MainScreenFragment probableFragment = getMainFragmentIfVisible();
showingMainFragmentFromOther = (probableFragment==null);
if (showingMainFragmentFromOther){
FragmentManager fraMan = getSupportFragmentManager();
Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG);
Log.d(DEBUG_TAG, "Requested main fragment, not visible. Search by TAG: "+fragment);
if(fragment!=null){
//the fragment is there but not shown
probableFragment = (MainScreenFragment) fragment;
// set the flag
probableFragment.setSuppressArrivalsReload(true);
showMainFragment(fraMan, probableFragment, true);
+ probableFragment.requestArrivalsForStopID(ID);
} else {
//createAndShowMainFragment
// we have no fragment
- probableFragment = MainScreenFragment.newInstance();
- showMainFragment(fraMan, probableFragment,true);
+ final Bundle args = new Bundle();
+ args.putString(MainScreenFragment.PENDING_STOP_SEARCH, ID);
+ //if onCreate is complete, then we are not asking for the first showing fragment
+ boolean addtobackstack = onCreateComplete;
+ createShowMainFragment(fraMan, args ,addtobackstack);
//probableFragment = createAndShowMainFragment();
}
}
- probableFragment.requestArrivalsForStopID(ID);
+
mNavView.setCheckedItem(R.id.nav_arrivals);
}
@Override
public void toggleSpinner(boolean state) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.toggleSpinner(state);
}
}
@Override
public void enableRefreshLayout(boolean yes) {
MainScreenFragment probableFragment = getMainFragmentIfVisible();
if (probableFragment!=null){
probableFragment.enableRefreshLayout(yes);
}
}
@Override
public void showMapCenteredOnStop(Stop stop) {
createAndShowMapFragment(stop, true);
}
//Map Fragment stuff
void createAndShowMapFragment(@Nullable Stop stop, boolean addToBackStack){
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
MapFragment fragment = stop == null? MapFragment.getInstance(): MapFragment.getInstance(stop);
ft.replace(R.id.mainActContentFrame, fragment, MapFragment.FRAGMENT_TAG);
if (addToBackStack) ft.addToBackStack(null);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
class ToolbarItemClickListener implements Toolbar.OnMenuItemClickListener{
private final Context activityContext;
public ToolbarItemClickListener(Context activityContext) {
this.activityContext = activityContext;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_about:
startActivity(new Intent(ActivityPrincipal.this, ActivityAbout.class));
return true;
case R.id.action_hack:
openIceweasel(getString(R.string.hack_url), activityContext);
return true;
case R.id.action_source:
openIceweasel("https://gitpull.it/source/libre-busto/", activityContext);
return true;
case R.id.action_licence:
openIceweasel("https://www.gnu.org/licenses/gpl-3.0.html", activityContext);
return true;
case R.id.action_experiments:
startActivity(new Intent(ActivityPrincipal.this, ActivityExperiments.class));
default:
}
return false;
}
}
}
diff --git a/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java
index 0374be0..cac9905 100644
--- a/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java
+++ b/src/it/reyboz/bustorino/backend/gtfs/PolylineParser.java
@@ -1,48 +1,48 @@
package it.reyboz.bustorino.backend.gtfs;
import org.osmdroid.util.GeoPoint;
import java.util.ArrayList;
-public final class PolylineParser {
+public abstract class PolylineParser {
/**
* Decode a Google polyline
* Thanks to https://stackoverflow.com/questions/9341020/how-to-decode-googles-polyline-algorithm
* @param encodedPolyline the encoded polyline in a string
* @param initial_capacity for the list
* @return the list of points correspoding to the polyline
*/
public static ArrayList decodePolyline(String encodedPolyline, int initial_capacity) {
ArrayList points = new ArrayList<>(initial_capacity);
int truck = 0;
int carriage_q = 0;
int longit=0, latit=0;
boolean is_lat=true;
for (int x = 0, xx = encodedPolyline.length(); x < xx; ++x) {
int i = encodedPolyline.charAt(x);
i -= 63;
int _5_bits = i << (32 - 5) >>> (32 - 5);
truck |= _5_bits << carriage_q;
carriage_q += 5;
boolean is_last = (i & (1 << 5)) == 0;
if (is_last) {
boolean is_negative = (truck & 1) == 1;
truck >>>= 1;
if (is_negative) {
truck = ~truck;
}
if (is_lat){
latit += truck;
is_lat = false;
} else{
longit += truck;
points.add(new GeoPoint((double)latit/1e5,(double)longit/1e5));
is_lat=true;
}
carriage_q = 0;
truck = 0;
}
}
return points;
}
}
diff --git a/src/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/src/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
new file mode 100644
index 0000000..0444f7f
--- /dev/null
+++ b/src/it/reyboz/bustorino/fragments/LinesDetailFragment.kt
@@ -0,0 +1,154 @@
+package it.reyboz.bustorino.fragments
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Spinner
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import it.reyboz.bustorino.R
+import it.reyboz.bustorino.backend.gtfs.PolylineParser
+import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
+import it.reyboz.bustorino.data.gtfs.PatternStop
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.Polyline
+
+class LinesDetailFragment() : Fragment() {
+
+ private lateinit var lineID: String
+
+
+ private lateinit var patternsSpinner: Spinner
+ private var patternsAdapter: ArrayAdapter? = null
+
+ private var patternsSpinnerState: Parcelable? = null
+
+ private lateinit var currentPatterns: List
+ private lateinit var gtfsStopsForCurrentPattern: List
+
+ private lateinit var map: MapView
+ private lateinit var polyLine: Polyline
+
+ private lateinit var viewingPattern: MatoPatternWithStops
+
+ private lateinit var viewModel: LinesViewModel
+
+
+
+ companion object {
+ private const val LINEID_KEY="lineID"
+ fun newInstance() = LinesDetailFragment()
+ const val DEBUG_TAG="LinesDetailFragment"
+
+ fun makeArgs(lineID: String): Bundle{
+ val b = Bundle()
+ b.putString(LINEID_KEY, lineID)
+ return b
+ }
+ }
+
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false)
+ lineID = requireArguments().getString(LINEID_KEY, "")
+
+ patternsSpinner = rootView.findViewById(R.id.patternsSpinner)
+ patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList())
+ patternsSpinner.adapter = patternsAdapter
+
+ map = rootView.findViewById(R.id.lineMap)
+ map.setTileSource(TileSourceFactory.MAPNIK)
+ //map.setTilesScaledToDpi(true);
+ //map.setTilesScaledToDpi(true);
+ map.setFlingEnabled(true)
+
+ // add ability to zoom with 2 fingers
+ map.setMultiTouchControls(true)
+ map.minZoomLevel = 13.0
+
+
+
+ viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){
+ patterns -> savePatternsToShow(patterns)
+ }
+
+
+ /*
+ We have the pattern and the stops here, time to display them
+ */
+ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops ->
+ Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}")
+
+ val pattern = viewingPattern.pattern
+
+ val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength)
+ val polyLine=Polyline(map)
+ polyLine.setPoints(pointsList)
+
+ map.overlayManager.add(polyLine)
+ map.controller.animateTo(pointsList[0])
+ }
+
+ viewModel.setRouteIDQuery(lineID)
+
+ Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}")
+
+ //listeners
+ patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
+ val patternWithStops = currentPatterns.get(position)
+ setPatternAndReqStops(patternWithStops)
+ }
+
+ override fun onNothingSelected(p0: AdapterView<*>?) {
+ }
+ }
+
+
+ return rootView
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel = ViewModelProvider(this).get(LinesViewModel::class.java)
+ }
+
+ private fun savePatternsToShow(patterns: List){
+ currentPatterns = patterns.sortedBy { p-> p.pattern.code }
+
+ patternsAdapter?.let {
+ it.clear()
+ it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" })
+ it.notifyDataSetChanged()
+ }
+
+ val pos = patternsSpinner.selectedItemPosition
+ //might be possible that the selectedItem is different (larger than list size)
+ if(pos!= AdapterView.INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){
+ val p = currentPatterns[pos]
+ Log.d(LinesFragment.DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}")
+ setPatternAndReqStops(currentPatterns[pos])
+ }
+ Log.d(DEBUG_TAG, "Patterns changed")
+
+ }
+
+ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){
+ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}")
+ gtfsStopsForCurrentPattern = patternWithStops.stopsIndices.sortedBy { i-> i.order }
+ viewingPattern = patternWithStops
+
+ viewModel.requestStopsForPatternWithStops(patternWithStops)
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/fragments/LinesFragment.kt b/src/it/reyboz/bustorino/fragments/LinesFragment.kt
index 2007aad..29b7a6f 100644
--- a/src/it/reyboz/bustorino/fragments/LinesFragment.kt
+++ b/src/it/reyboz/bustorino/fragments/LinesFragment.kt
@@ -1,392 +1,393 @@
/*
BusTO - Fragments components
Copyright (C) 2022 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.fragments
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import android.view.*
import android.widget.*
import android.widget.AdapterView.INVALID_POSITION
import android.widget.AdapterView.OnItemSelectedListener
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import it.reyboz.bustorino.R
import it.reyboz.bustorino.adapters.NameCapitalize
import it.reyboz.bustorino.adapters.StopAdapterListener
import it.reyboz.bustorino.adapters.StopRecyclerAdapter
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
import it.reyboz.bustorino.data.gtfs.PatternStop
import it.reyboz.bustorino.util.LinesNameSorter
import it.reyboz.bustorino.util.PatternWithStopsSorter
class LinesFragment : ScreenBaseFragment() {
companion object {
fun newInstance(){
LinesFragment()
}
const val DEBUG_TAG="BusTO-LinesFragment"
const val FRAGMENT_TAG="LinesFragment"
val patternStopsComparator = PatternWithStopsSorter()
}
private lateinit var viewModel: LinesViewModel
private lateinit var linesSpinner: Spinner
private lateinit var patternsSpinner: Spinner
private lateinit var currentRoutes: List
private lateinit var currentPatterns: List
private lateinit var currentPatternStops: List
private lateinit var routeDescriptionTextView: TextView
private lateinit var stopsRecyclerView: RecyclerView
private var linesAdapter: ArrayAdapter? = null
private var patternsAdapter: ArrayAdapter? = null
private var mListener: CommonFragmentListener? = null
private val linesNameSorter = LinesNameSorter()
private val linesComparator = Comparator { a,b ->
return@Comparator linesNameSorter.compare(a.shortName, b.shortName)
}
private var firstClick = true
private var recyclerViewState:Parcelable? = null
private var patternsSpinnerState:Parcelable? = null
private val adapterListener = object : StopAdapterListener {
override fun onTappedStop(stop: Stop?) {
//var r = ""
//stop?.let { r= it.stopDisplayName.toString() }
if(viewModel.shouldShowMessage) {
Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show()
viewModel.shouldShowMessage=false
}
stop?.let {
mListener?.requestArrivalsForStopID(it.ID)
}
if(stop == null){
Log.e(DEBUG_TAG,"Passed wrong stop")
}
if(mListener == null){
Log.e(DEBUG_TAG, "Listener is null")
}
}
override fun onLongPressOnStop(stop: Stop?): Boolean {
Log.d(DEBUG_TAG, "LongPressOnStop")
return true
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.d(DEBUG_TAG, "saveInstanceState bundle: $outState")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.fragment_lines, container, false)
linesSpinner = rootView.findViewById(R.id.linesSpinner)
patternsSpinner = rootView.findViewById(R.id.patternsSpinner)
routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView)
stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView)
val llManager = LinearLayoutManager(context)
llManager.orientation = LinearLayoutManager.VERTICAL
stopsRecyclerView.layoutManager = llManager
//allow the context menu to be opened
registerForContextMenu(stopsRecyclerView)
Log.d(DEBUG_TAG, "Called onCreateView for LinesFragment")
Log.d(DEBUG_TAG, "OnCreateView, selected line spinner pos: ${linesSpinner.selectedItemPosition}")
Log.d(DEBUG_TAG, "OnCreateView, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}")
//set requests
viewModel.routesGTTLiveData.observe(viewLifecycleOwner) {
setRoutes(it)
}
viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){
patterns ->
run {
currentPatterns = patterns.sortedBy { p-> p.pattern.code }
//patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" }
patternsAdapter?.let {
it.clear()
it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" })
it.notifyDataSetChanged()
}
val pos = patternsSpinner.selectedItemPosition
//might be possible that the selectedItem is different (larger than list size)
if(pos!= INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){
val p = currentPatterns[pos]
Log.d(DEBUG_TAG, "Setting patterns with pos $pos and p gtfsID ${p.pattern.code}")
setPatternAndReqStops(currentPatterns[pos])
}
}
}
viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner){stops->
Log.d("BusTO-LinesFragment", "Setting stops from DB")
setCurrentStops(stops)
}
if(context!=null) {
patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList())
patternsSpinner.adapter = patternsAdapter
linesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList())
linesSpinner.adapter = linesAdapter
if (linesSpinner.onItemSelectedListener != null){
Log.d(DEBUG_TAG, "linesSpinner listener != null")
}
+ //listener
linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) {
val selRoute = currentRoutes.get(pos)
routeDescriptionTextView.text = selRoute.longName
val oldRoute = viewModel.getRouteIDQueried()
val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim())
Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner, oldRoute: $oldRoute")
//launch query for this gtfsID
viewModel.setRouteIDQuery(selRoute.gtfsId)
//reset spinner position
if(resetSpinner) patternsSpinner.setSelection(0)
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
val patternWithStops = currentPatterns.get(position)
//
setPatternAndReqStops(patternWithStops)
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
}
return rootView
}
override fun onAttach(context: Context) {
super.onAttach(context)
if(context is CommonFragmentListener)
mListener = context
else throw RuntimeException(context.toString()
+ " must implement CommonFragmentListener")
}
override fun onResume() {
super.onResume()
mListener?.readyGUIfor(FragmentKind.LINES)
Log.d(DEBUG_TAG, "Resuming lines fragment")
//Log.d(DEBUG_TAG, "OnResume, selected line spinner pos: ${linesSpinner.selectedItemPosition}")
//Log.d(DEBUG_TAG, "OnResume, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(LinesViewModel::class.java)
Log.d(DEBUG_TAG, "Fragment onCreate")
}
override fun getBaseViewForSnackBar(): View? {
return null
}
private fun setRoutes(routes: List){
Log.d(DEBUG_TAG, "Resetting routes")
currentRoutes = routes.sortedWith(linesComparator)
if (linesAdapter!=null){
var selGtfsRoute = viewModel.getRouteIDQueried()
var selPatternIndex = 0
if(selGtfsRoute == null){
selGtfsRoute =""
}
val adapter = linesAdapter!!
if (adapter.isEmpty) {
Log.d(DEBUG_TAG, "Lines adapter is empty")
}
else{
adapter.clear()
}
adapter.addAll(currentRoutes.map { r -> r.shortName })
adapter.notifyDataSetChanged()
for(j in 0 until currentRoutes.size){
val route = currentRoutes[j]
if (route.gtfsId == selGtfsRoute) {
selPatternIndex = j
Log.d(DEBUG_TAG, "Route $selGtfsRoute has index $j")
}
}
linesSpinner.setSelection(selPatternIndex)
//
}
/*
linesAdapter?.clear()
linesAdapter?.addAll(currentRoutes.map { r -> r.shortName })
linesAdapter?.notifyDataSetChanged()
*/
}
private fun setCurrentStops(stops: List){
val orderBy = currentPatternStops.withIndex().associate{it.value.stopGtfsId to it.index}
val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] }
val numStops = stopsSorted.size
Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}")
var setNewAdapter = true
if(stopsRecyclerView.adapter is StopRecyclerAdapter){
val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter
if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID)
&& (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID)
){
Log.d(DEBUG_TAG, "Found same stops on recyclerview")
setNewAdapter = false
}
/*else {
Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops")
adapter.stops = stopsSorted
adapter.notifyDataSetChanged()
}*/
}
if(setNewAdapter){
stopsRecyclerView.adapter = StopRecyclerAdapter(
stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES,
NameCapitalize.FIRST
)
}
}
private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){
Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}")
currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order }
viewModel.requestStopsForPatternWithStops(patternWithStops)
}
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
Log.d("BusTO-LinesFragment", "Creating context menu ")
if (v.id == R.id.patternStopsRecyclerView) {
// if we aren't attached to activity, return null
if (activity == null) return
val inflater = requireActivity().menuInflater
inflater.inflate(R.menu.menu_line_item, menu)
}
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false
val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter
val stop = adapter.stops.get(adapter.getPosition())
val acId = item.itemId
if(acId == R.id.action_view_on_map){
// view on the map
if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) {
Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show()
return true
}
mListener!!.showMapCenteredOnStop(stop)
return true
} else if (acId == R.id.action_show_arrivals){
mListener?.requestArrivalsForStopID(stop.ID)
return true
}
return false
}
override fun onStop() {
super.onStop()
Log.d(DEBUG_TAG, "Fragment stopped")
recyclerViewState = stopsRecyclerView.layoutManager?.onSaveInstanceState()
patternsSpinnerState = patternsSpinner.onSaveInstanceState()
}
override fun onStart() {
super.onStart()
Log.d(DEBUG_TAG, "OnStart, selected line spinner pos: ${linesSpinner.selectedItemPosition}")
Log.d(DEBUG_TAG, "OnStart, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}")
if (recyclerViewState!=null){
stopsRecyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
}
if(patternsSpinnerState!=null){
patternsSpinner.onRestoreInstanceState(patternsSpinnerState)
}
}
/*
override fun onDestroyView() {
super.onDestroyView()
Log.d(DEBUG_TAG, "Fragment view destroyed")
}
override fun onDestroy() {
super.onDestroy()
Log.d(DEBUG_TAG, "Fragment destroyed")
}
*/
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
Log.d(DEBUG_TAG, "OnViewStateRes, bundled saveinstancestate: $savedInstanceState")
Log.d(DEBUG_TAG, "OnViewStateRes, selected line spinner pos: ${linesSpinner.selectedItemPosition}")
Log.d(DEBUG_TAG, "OnViewStateRes, selected patterns spinner pos: ${patternsSpinner.selectedItemPosition}")
}
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt
index 2bab099..50b4c59 100644
--- a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt
+++ b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt
@@ -1,88 +1,91 @@
package it.reyboz.bustorino.fragments
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import it.reyboz.bustorino.backend.Stop
import it.reyboz.bustorino.data.GtfsRepository
import it.reyboz.bustorino.data.NextGenDB
import it.reyboz.bustorino.data.OldDataRepository
import it.reyboz.bustorino.data.gtfs.GtfsDatabase
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops
import java.util.concurrent.Executors
class LinesViewModel(application: Application) : AndroidViewModel(application) {
private val gtfsRepo: GtfsRepository
private val oldRepo: OldDataRepository
//val patternsByRouteLiveData: LiveData>
private val routeIDToSearch = MutableLiveData()
private var lastShownPatternStops = ArrayList()
val stopsForPatternLiveData = MutableLiveData>()
- val executor = Executors.newFixedThreadPool(2)
+ private val executor = Executors.newFixedThreadPool(2)
init {
val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao()
gtfsRepo = GtfsRepository(gtfsDao)
oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application))
}
val routesGTTLiveData: LiveData> by lazy{
gtfsRepo.getLinesLiveDataForFeed("gtt")
}
val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap {
gtfsRepo.getPatternsWithStopsForRouteID(it)
}
fun setRouteIDQuery(routeID: String){
routeIDToSearch.value = routeID
}
fun getRouteIDQueried(): String?{
return routeIDToSearch.value
}
var shouldShowMessage = true
+ /**
+ * Find the
+ */
private fun requestStopsForGTFSIDs(gtfsIDs: List){
if (gtfsIDs.equals(lastShownPatternStops)){
//nothing to do
return
}
oldRepo.requestStopsWithGtfsIDs(gtfsIDs) {
if (it.isSuccess) {
stopsForPatternLiveData.postValue(it.result)
} else {
Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID")
it.exception?.printStackTrace()
}
}
lastShownPatternStops.clear()
for(id in gtfsIDs)
lastShownPatternStops.add(id)
}
fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){
val gtfsIDs = ArrayList()
for(pat in patternStops.stopsIndices){
gtfsIDs.add(pat.stopGtfsId)
}
requestStopsForGTFSIDs(gtfsIDs)
}
/*fun getLinesGTT(): MutableLiveData> {
val routesData = MutableLiveData>()
viewModelScope.launch {
val routes=gtfsRepo.getLinesForFeed("gtt")
routesData.postValue(routes)
}
return routesData
}*/
}
\ No newline at end of file
diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
index 1293e8e..a62eff8 100644
--- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
+++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java
@@ -1,752 +1,803 @@
package it.reyboz.bustorino.fragments;
import android.Manifest;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.zxing.integration.android.IntentIntegrator;
import java.util.Map;
import it.reyboz.bustorino.R;
import it.reyboz.bustorino.backend.*;
import it.reyboz.bustorino.middleware.AppLocationManager;
import it.reyboz.bustorino.middleware.AsyncArrivalsSearcher;
import it.reyboz.bustorino.middleware.AsyncStopsSearcher;
+import it.reyboz.bustorino.middleware.BarcodeScanContract;
+import it.reyboz.bustorino.middleware.BarcodeScanOptions;
+import it.reyboz.bustorino.middleware.BarcodeScanUtils;
import it.reyboz.bustorino.util.LocationCriteria;
import it.reyboz.bustorino.util.Permissions;
+import static it.reyboz.bustorino.backend.utils.getBusStopIDFromUri;
import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS;
import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN;
/**
* A simple {@link Fragment} subclass.
* Use the {@link MainScreenFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class MainScreenFragment extends ScreenBaseFragment implements FragmentListenerMain{
private static final String OPTION_SHOW_LEGEND = "show_legend";
private static final String SAVED_FRAGMENT="saved_fragment";
private static final String DEBUG_TAG = "BusTO - MainFragment";
+ public static final String PENDING_STOP_SEARCH="PendingStopSearch";
+
public final static String FRAGMENT_TAG = "MainScreenFragment";
/// UI ELEMENTS //
private ImageButton addToFavorites;
private FragmentHelper fragmentHelper;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText busStopSearchByIDEditText;
private EditText busStopSearchByNameEditText;
private ProgressBar progressBar;
private TextView howDoesItWorkTextView;
private Button hideHintButton;
private MenuItem actionHelpMenuItem;
private FloatingActionButton floatingActionButton;
- private boolean setupOnAttached = true;
+ private boolean setupOnResume = true;
private boolean suppressArrivalsReload = false;
+ private boolean instanceStateSaved = false;
//private Snackbar snackbar;
/*
* Search mode
*/
private static final int SEARCH_BY_NAME = 0;
private static final int SEARCH_BY_ID = 1;
private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12
private int searchMode;
//private ImageButton addToFavorites;
private final ArrivalsFetcher[] arrivalsFetchers = utils.getDefaultArrivalsFetchers();
//// HIDDEN BUT IMPORTANT ELEMENTS ////
FragmentManager fragMan;
Handler mainHandler;
private final Runnable refreshStop = new Runnable() {
public void run() {
if(getContext() == null) return;
if (fragMan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) {
ArrivalsFragment fragment = (ArrivalsFragment) fragMan.findFragmentById(R.id.resultFrame);
if (fragment == null){
//we create a new fragment, which is WRONG
Log.e("BusTO-RefreshStop", "Asking for refresh when there is no fragment");
// AsyncDataDownload(fragmentHelper, arrivalsFetchers,getContext()).execute();
} else{
String stopName = fragment.getStopID();
new AsyncArrivalsSearcher(fragmentHelper, fragment.getCurrentFetchersAsArray(), getContext()).execute(stopName);
}
} else //we create a new fragment, which is WRONG
new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute();
}
};
+ //
+ private final ActivityResultLauncher barcodeLauncher = registerForActivityResult(new BarcodeScanContract(),
+ result -> {
+ if(result!=null && result.getContents()!=null) {
+ //Toast.makeText(MyActivity.this, "Cancelled", Toast.LENGTH_LONG).show();
+ Uri uri;
+ try {
+ uri = Uri.parse(result.getContents()); // this apparently prevents NullPointerException. Somehow.
+ } catch (NullPointerException e) {
+ if (getContext()!=null)
+ Toast.makeText(getContext().getApplicationContext(),
+ R.string.no_qrcode, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ String busStopID = getBusStopIDFromUri(uri);
+ busStopSearchByIDEditText.setText(busStopID);
+ requestArrivalsForStopID(busStopID);
+
+ } else {
+ //Toast.makeText(MyActivity.this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show();
+ if (getContext()!=null)
+ Toast.makeText(getContext().getApplicationContext(),
+ R.string.no_qrcode, Toast.LENGTH_SHORT).show();
+
+
+
+ }
+ });
/// LOCATION STUFF ///
boolean pendingNearbyStopsRequest = false;
boolean locationPermissionGranted, locationPermissionAsked = false;
AppLocationManager locationManager;
private final LocationCriteria cr = new LocationCriteria(2000, 10000);
//Location
private AppLocationManager.LocationRequester requester = new AppLocationManager.LocationRequester() {
@Override
public void onLocationChanged(Location loc) {
}
@Override
public void onLocationStatusChanged(int status) {
if(status == AppLocationManager.LOCATION_GPS_AVAILABLE && !isNearbyFragmentShown()){
//request Stops
pendingNearbyStopsRequest = false;
if (getContext()!= null)
mainHandler.post(new NearbyStopsRequester(getContext(), cr));
}
}
@Override
public long getLastUpdateTimeMillis() {
return 50;
}
@Override
public LocationCriteria getLocationCriteria() {
return cr;
}
@Override
public void onLocationProviderAvailable() {
//Log.w(DEBUG_TAG, "pendingNearbyStopRequest: "+pendingNearbyStopsRequest);
if(!isNearbyFragmentShown() && getContext()!=null){
pendingNearbyStopsRequest = false;
mainHandler.post(new NearbyStopsRequester(getContext(), cr));
}
}
@Override
public void onLocationDisabled() {
}
};
private final ActivityResultLauncher requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback