diff --git a/build.gradle b/build.gradle index 3b4be30..c2a20dd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,144 +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" acra_version = "5.7.0" lifecycle_version = "2.3.1" arch_version = "2.1.0" room_version = "2.3.0" //kotlin kotlin_version = '1.6.0' coroutines_version = "1.5.0" } dependencies { classpath 'com.android.tools.build:gradle:4.1.3' 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 buildToolsVersion '30.0.3' defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 16 targetSdkVersion 30 versionCode 36 versionName "1.16.0" 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.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 '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" // 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/res/layout/fragment_main_screen.xml b/res/layout/fragment_main_screen.xml index 746319c..b4357aa 100644 --- a/res/layout/fragment_main_screen.xml +++ b/res/layout/fragment_main_screen.xml @@ -1,164 +1,177 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".fragments.MainScreenFragment" android:paddingTop="10dip" > <ImageButton android:id="@+id/QRButton" style="?android:attr/borderlessButtonStyle" android:layout_width="30dip" android:layout_height="30dip" android:layout_alignBottom="@+id/searchButton" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_marginLeft="16dp" android:layout_marginStart="16dp" android:background="@drawable/qrcode_button_custom" android:contentDescription="@string/qrcode" /> <EditText android:id="@+id/busStopSearchByIDEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="5dp" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_toEndOf="@+id/QRButton" android:layout_toLeftOf="@+id/searchButton" android:layout_toRightOf="@+id/QRButton" android:layout_toStartOf="@+id/searchButton" android:ems="10" android:hint="@string/insert_bus_stop_number" android:imeOptions="actionSearch" android:inputType="number" android:selectAllOnFocus="true" android:singleLine="true" > <requestFocus /> </EditText> <!--android:layout_alignParentLeft="true" android:layout_alignParentStart="true"--> <EditText android:id="@+id/busStopSearchByNameEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="5dp" android:layout_marginLeft="5dip" android:layout_marginRight="5dip" android:layout_toEndOf="@+id/QRButton" android:layout_toLeftOf="@+id/searchButton" android:layout_toRightOf="@+id/QRButton" android:layout_toStartOf="@+id/searchButton" - + android:inputType="text" android:ems="10" android:hint="@string/insert_bus_stop_name" android:imeOptions="actionSearch" android:singleLine="true" android:visibility="gone"> <requestFocus /> </EditText> <ImageButton android:id="@+id/searchButton" style="?android:attr/borderlessButtonStyle" android:layout_width="40dip" android:layout_height="40dip" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_alignTop="@+id/busStopSearchByIDEditText" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:background="@drawable/search_button_custom" android:contentDescription="@string/search" /> <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" android:layout_alignParentStart="true" android:layout_below="@+id/searchButton" android:layout_marginEnd="16dp" android:layout_marginLeft="16dip" android:layout_marginRight="16dp" android:layout_marginStart="16dip" android:indeterminateOnly="true" android:visibility="gone" /> <TextView android:id="@+id/howDoesItWorkTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="10dp" + android:layout_marginBottom="5dp" android:layout_marginEnd="16dp" android:layout_marginLeft="16dip" android:layout_marginRight="16dp" android:layout_marginStart="16dip" + android:layout_marginTop="10dp" android:layout_toLeftOf="@+id/hideHintButton" android:layout_toStartOf="@+id/hideHintButton" android:text="@string/howDoesItWork" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="@color/blue_500" android:visibility="gone" android:layout_below="@+id/searchButton"/> <Button android:id="@+id/hideHintButton" style="?android:attr/buttonStyleSmall" android:layout_width="85dip" android:layout_height="wrap_content" android:layout_alignBottom="@+id/howDoesItWorkTextView" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" android:background="@drawable/route_background_bus" android:text="@string/hint_button" android:textColor="@color/grey_100" android:textSize="19sp" android:visibility="gone" /> - <androidx.swiperefreshlayout.widget.SwipeRefreshLayout - android:id="@+id/listRefreshLayout" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" - android:layout_below="@+id/howDoesItWorkTextView" - android:animateLayoutChanges="true" - android:visibility="gone"> - <FrameLayout - android:layout_width="match_parent" - android:id="@+id/resultFrame" - android:layout_height="fill_parent" - android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/> - </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> - <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/floatingActionButton" - android:layout_width="wrap_content" + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:layout_width="match_parent" android:layout_height="wrap_content" + android:id="@+id/coord_layout" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_gravity="bottom|end" - android:layout_margin="16dp" - android:src="@drawable/alphabetical" - fabSize="normal" - backgroundTint="@color/teal_500" - rippleColor="@color/teal_300" - elevation="15dp" + android:layout_below="@+id/howDoesItWorkTextView" - /> + > + <androidx.swiperefreshlayout.widget.SwipeRefreshLayout + android:id="@+id/listRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:animateLayoutChanges="true" + android:visibility="gone"> + + <FrameLayout + android:layout_width="match_parent" + android:id="@+id/resultFrame" + android:layout_height="fill_parent" + android:layout_alignParentLeft="true" android:layout_alignParentStart="true"/> + </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/floatingActionButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:layout_gravity="bottom|end" + android:layout_margin="13dp" + android:src="@drawable/alphabetical" + fabSize="normal" + backgroundTint="@color/teal_500" + rippleColor="@color/teal_300" + elevation="13dp" + + /> + </androidx.coordinatorlayout.widget.CoordinatorLayout> </RelativeLayout> \ No newline at end of file diff --git a/src/it/reyboz/bustorino/ActivityPrincipal.java b/src/it/reyboz/bustorino/ActivityPrincipal.java index 2cca929..5913140 100644 --- a/src/it/reyboz/bustorino/ActivityPrincipal.java +++ b/src/it/reyboz/bustorino/ActivityPrincipal.java @@ -1,590 +1,595 @@ /* 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 <http://www.gnu.org/licenses/>. */ 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.BackoffPolicy; import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; 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 java.util.concurrent.TimeUnit; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.DBUpdateWorker; import it.reyboz.bustorino.data.DatabaseUpdate; -import it.reyboz.bustorino.fragments.FavoritesFragment; -import it.reyboz.bustorino.fragments.FragmentKind; -import it.reyboz.bustorino.fragments.FragmentListenerMain; -import it.reyboz.bustorino.fragments.MainScreenFragment; -import it.reyboz.bustorino.fragments.MapFragment; +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; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_principal); final SharedPreferences theShPr = getMainSharedPreferences(); 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); 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); } //Try (hopefully) database update DatabaseUpdate.requestDBUpdateWithWork(this, 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 showMainFragment(); } 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 FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); FavoritesFragment fragment = FavoritesFragment.newInstance(); ft.replace(R.id.mainActContentFrame,fragment, TAG_FAVORITES); ft.addToBackStack(null); ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.commit(); return true; } else if(menuItem.getItemId() == R.id.nav_arrivals){ closeDrawerIfOpen(); showMainFragment(); return true; } else if(menuItem.getItemId() == R.id.nav_map_item){ closeDrawerIfOpen(); final String permission = Manifest.permission.WRITE_EXTERNAL_STORAGE; int result = askForPermissionIfNeeded(permission, STORAGE_PERMISSION_REQ); switch (result) { case PERMISSION_OK: createAndShowMapFragment(null); break; case PERMISSION_ASKING: permissionDoneRunnables.put(permission, () -> createAndShowMapFragment(null)); 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(); } 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().popBackStackImmediate(); if(showingMainFragmentFromOther && getSupportFragmentManager().getBackStackEntryCount() > 0){ getSupportFragmentManager().popBackStack(); } } else if (getSupportFragmentManager().getBackStackEntryCount() > 0) { getSupportFragmentManager().popBackStack(); } else super.onBackPressed(); } + /** + * Create and show the SnackBar with the message + */ private void createDefaultSnackbar() { - if (snackbar == null) { - snackbar = Snackbar.make(findViewById(R.id.searchButton), R.string.database_update_message, Snackbar.LENGTH_INDEFINITE); + + 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_message, Snackbar.LENGTH_INDEFINITE); snackbar.show(); } private MainScreenFragment createAndShowMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); MainScreenFragment fragment = MainScreenFragment.newInstance(); FragmentTransaction transaction = fraMan.beginTransaction(); transaction.replace(R.id.mainActContentFrame, fragment, MainScreenFragment.FRAGMENT_TAG); transaction.commit(); return fragment; } /** * 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){ fraMan.beginTransaction().replace(R.id.mainActContentFrame, fragment) .setReorderingAllowed(true) .addToBackStack(null) /*.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) .commit(); } private MainScreenFragment showMainFragment(){ FragmentManager fraMan = getSupportFragmentManager(); Fragment fragment = fraMan.findFragmentByTag(MainScreenFragment.FRAGMENT_TAG); final MainScreenFragment mainScreenFragment; if (fragment==null | !(fragment instanceof MainScreenFragment)){ mainScreenFragment = createAndShowMainFragment(); } else if(!fragment.isVisible()){ mainScreenFragment = (MainScreenFragment) fragment; showMainFragment(fraMan, mainScreenFragment); 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; 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); if(fragment!=null){ //the fragment is there but not shown probableFragment = (MainScreenFragment) fragment; // set the flag probableFragment.setSuppressArrivalsReload(true); showMainFragment(fraMan, probableFragment); } else { // we have no fragment 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); } //Map Fragment stuff void createAndShowMapFragment(@Nullable Stop stop){ 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); 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/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt index 057400f..7f2152f 100644 --- a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -1,268 +1,268 @@ /* BusTO - Backend components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.backend.mato import android.content.Context import android.util.Log import com.android.volley.toolbox.RequestFuture import it.reyboz.bustorino.BuildConfig import it.reyboz.bustorino.backend.* import org.json.JSONObject import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference open class MatoAPIFetcher(val minNumPassaggi: Int) : ArrivalsFetcher { var appContext: Context? = null set(value) { field = value!!.applicationContext } constructor(): this(2) override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference<Fetcher.Result>?): Palina { stopID!! val now = Calendar.getInstance().time var numMinutes = 0 var palina = Palina(stopID) var numPassaggi = 0 var trials = 0 val numDepartures = 4 while (numPassaggi < minNumPassaggi && trials < 4) { //numDepartures+=2 numMinutes += 20 val future = RequestFuture.newFuture<Palina>() val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future) if (appContext == null || res == null) { Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref") return Palina(stopID) } val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue request.setTag(getVolleyReqTag(QueryType.ARRIVALS)) requestQueue.add(request) try { val palinaResult = future.get(5, TimeUnit.SECONDS) if (palinaResult!=null) { if (BuildConfig.DEBUG) for (r in palinaResult.queryAllRoutes()){ Log.d(DEBUG_TAG, "route " + r.gtfsId + " has " + r.passaggi.size + " passaggi: "+ r.passaggiToString) } palina = palinaResult numPassaggi = palina.minNumberOfPassages } else{ Log.d(DEBUG_TAG, "Result palina is null") } } catch (e: InterruptedException) { e.printStackTrace() res.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() if (res.get() == Fetcher.Result.OK) res.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } trials++ } return palina } override fun getSourceForFetcher(): Passaggio.Source { return Passaggio.Source.MatoAPI } companion object{ const val VOLLEY_TAG = "MatoAPIFetcher" const val DEBUG_TAG = "BusTO:MatoAPIFetcher" val REQ_PARAMETERS = mapOf( "Content-Type" to "application/json; charset=utf-8", "DNT" to "1", "Host" to "mapi.5t.torino.it") fun getVolleyReqTag(type: QueryType): String{ return when (type){ QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops" QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals" } } /** * Get stops from the MatoAPI, set [res] accordingly */ fun getAllStopsGTT(context: Context, res: AtomicReference<Fetcher.Result>?): List<Palina>{ val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue val future = RequestFuture.newFuture<List<Palina>>() val request = VolleyAllStopsRequest(future, future) request.tag = getVolleyReqTag(QueryType.ALL_STOPS) requestQueue.add(request) - var palinaList:List<Palina> = mutableListOf<Palina>() + var palinaList:List<Palina> = mutableListOf() try { - palinaList = future.get(30, TimeUnit.SECONDS) + palinaList = future.get(60, TimeUnit.SECONDS) res?.set(Fetcher.Result.OK) }catch (e: InterruptedException) { e.printStackTrace() res?.set(Fetcher.Result.PARSER_ERROR) } catch (e: ExecutionException) { e.printStackTrace() res?.set(Fetcher.Result.SERVER_ERROR) } catch (e: TimeoutException) { res?.set(Fetcher.Result.CONNECTION_ERROR) e.printStackTrace() } return palinaList } /* fun makeRequest(type: QueryType?, variables: JSONObject) : String{ type.let { val requestData = JSONObject() when (it){ QueryType.ARRIVALS ->{ requestData.put("operationName","AllStopsDirect") requestData.put("variables", variables) requestData.put("query", MatoQueries.QUERY_ARRIVALS) } else -> { //TODO all other cases } } //todo make the request... //https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html //https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley } return "" } */ fun parseStopJSON(jsonStop: JSONObject): Palina{ val latitude = jsonStop.getDouble("lat") val longitude = jsonStop.getDouble("lon") val palina = Palina( jsonStop.getString("code"), jsonStop.getString("name"), null, null, latitude, longitude ) palina.gtfsID = jsonStop.getString("gtfsId") val routesStoppingJSON = jsonStop.getJSONArray("routes") val baseRoutes = mutableListOf<Route>() // get all the possible routes for (i in 0 until routesStoppingJSON.length()){ val routeBaseInfo = routesStoppingJSON.getJSONObject(i) val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"") r.setGtfsId(routeBaseInfo.getString("gtfsId").trim()) baseRoutes.add(r) } if (jsonStop.has("desc")){ palina.location = jsonStop.getString("desc") } //there is also "zoneId" which is the zone of the stop (0-> city, etc) if(jsonStop.has("stoptimesForPatterns")) { val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") for (i in 0 until routesStopTimes.length()) { val patternJSON = routesStopTimes.getJSONObject(i) val mRoute = parseRouteStoptimesJSON(patternJSON) //Log.d("BusTO-MapiFetcher") //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") //TODO: use directionId palina.addRoute(mRoute) for (r in baseRoutes) { if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) { baseRoutes.remove(r) break } } } } for (noArrivalRoute in baseRoutes){ palina.addRoute(noArrivalRoute) } //val gtfsRoutes = mutableListOf<>() return palina } fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{ val patternJSON = jsonPatternWithStops.getJSONObject("pattern") val routeJSON = patternJSON.getJSONObject("route") val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes") val gtfsId = routeJSON.getString("gtfsId").trim() val passages = mutableListOf<Passaggio>() for( i in 0 until passaggiJSON.length()){ val stoptime = passaggiJSON.getJSONObject(i) val scheduledTime = stoptime.getInt("scheduledArrival") val realtimeTime = stoptime.getInt("realtimeArrival") val realtime = stoptime.getBoolean("realtime") passages.add( Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime, Passaggio.Source.MatoAPI) ) } var routeType = Route.Type.UNKNOWN if (gtfsId[gtfsId.length-1] == 'E') routeType = Route.Type.LONG_DISTANCE_BUS else when( routeJSON.getString("mode").trim()){ "BUS" -> routeType = Route.Type.BUS "TRAM" -> routeType = Route.Type.TRAM } val route = Route( routeJSON.getString("shortName"), patternJSON.getString("headsign"), routeType, passages, ) route.setGtfsId(gtfsId) return route } fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{ val data = JSONObject() data.put("operationName", requestName) data.put("variables", variables) data.put("query", query) return data } } enum class QueryType { ARRIVALS, ALL_STOPS } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java index 3c45a71..d44a58e 100644 --- a/src/it/reyboz/bustorino/fragments/FavoritesFragment.java +++ b/src/it/reyboz/bustorino/fragments/FavoritesFragment.java @@ -1,310 +1,299 @@ /* BusTO - Fragments components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.fragments; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.*; import it.reyboz.bustorino.adapters.AdapterListener; -import it.reyboz.bustorino.adapters.StopAdapter; import it.reyboz.bustorino.adapters.StopRecyclerAdapter; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.FavoritesViewModel; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; -public class FavoritesFragment extends BaseFragment { +public class FavoritesFragment extends ScreenBaseFragment { private RecyclerView favoriteRecyclerView; private EditText busStopNameText; private TextView favoriteTipTextView; private ImageView angeryBusImageView; - private LinearLayoutManager llManager; @Nullable private CommonFragmentListener mListener; public static final String FRAGMENT_TAG = "BusTOFavFragment"; private final AdapterListener adapterListener = new AdapterListener() { @Override public void onTappedStop(Stop stop) { mListener.requestArrivalsForStopID(stop.ID); } }; public static FavoritesFragment newInstance() { FavoritesFragment fragment = new FavoritesFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } public FavoritesFragment(){ } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_favorites, container, false); favoriteRecyclerView = root.findViewById(R.id.favoritesRecyclerView); //favoriteListView = root.findViewById(R.id.favoriteListView); /*favoriteRecyclerView.setOn((parent, view, position, id) -> { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ /* Stop busStop = (Stop) parent.getItemAtPosition(position); if(mListener!=null){ mListener.requestArrivalsForStopID(busStop.ID); } }); */ - llManager = new LinearLayoutManager(getContext()); + LinearLayoutManager llManager = new LinearLayoutManager(getContext()); llManager.setOrientation(LinearLayoutManager.VERTICAL); favoriteRecyclerView.setLayoutManager(llManager); DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(favoriteRecyclerView.getContext(), llManager.getOrientation()); favoriteRecyclerView.addItemDecoration(dividerItemDecoration); angeryBusImageView = root.findViewById(R.id.angeryBusImageView); favoriteTipTextView = root.findViewById(R.id.favoriteTipTextView); registerForContextMenu(favoriteRecyclerView); FavoritesViewModel model = new ViewModelProvider(this).get(FavoritesViewModel.class); model.getFavorites().observe(getViewLifecycleOwner(), this::showStops); showStops(new ArrayList<>()); return root; } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { - throw new RuntimeException(context.toString() + throw new RuntimeException(context + " must implement CommonFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /* This method is apparently NOT CALLED ANYMORE */ @Override public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); Log.d("Favorites Fragment", "Creating context menu on "+v); if (v.getId() == R.id.favoritesRecyclerView) { // if we aren't attached to activity, return null if (getActivity()==null) return; MenuInflater inflater = getActivity().getMenuInflater(); inflater.inflate(R.menu.menu_favourites_entry, menu); } } @Override public void onResume() { super.onResume(); if (mListener!=null) mListener.readyGUIfor(FragmentKind.FAVORITES); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item .getMenuInfo(); if(!(favoriteRecyclerView.getAdapter() instanceof StopRecyclerAdapter)) return false; StopRecyclerAdapter adapter = (StopRecyclerAdapter) favoriteRecyclerView.getAdapter(); - Stop busStop = (Stop) adapter.getStops().get(adapter.getPosition()); + Stop busStop = adapter.getStops().get(adapter.getPosition()); switch (item.getItemId()) { case R.id.action_favourite_entry_delete: if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.REMOVE, result -> { }).execute(busStop); return true; case R.id.action_rename_bus_stop_username: showBusStopUsernameInputDialog(busStop); return true; case R.id.action_view_on_map: if (busStop.getLatitude() == null | busStop.getLongitude() == null | mListener==null ) { Toast.makeText(getContext(), R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show(); return true; } //GeoPoint point = new GeoPoint(busStop.getLatitude(), busStop.getLongitude()); mListener.showMapCenteredOnStop(busStop); return true; default: return super.onContextItemSelected(item); } } + @Nullable + @Override + public View getBaseViewForSnackBar() { + return null; + } void showStops(List<Stop> busStops){ // If no data is found show a friendly message if(BuildConfig.DEBUG) Log.d("BusTO - Favorites", "We have "+busStops.size()+" favorites in the list"); if (busStops.size() == 0) { favoriteRecyclerView.setVisibility(View.INVISIBLE); // TextView favoriteTipTextView = (TextView) findViewById(R.id.favoriteTipTextView); //assert favoriteTipTextView != null; favoriteTipTextView.setVisibility(View.VISIBLE); //ImageView angeryBusImageView = (ImageView) findViewById(R.id.angeryBusImageView); angeryBusImageView.setVisibility(View.VISIBLE); } else { favoriteRecyclerView.setVisibility(View.VISIBLE); favoriteTipTextView.setVisibility(View.INVISIBLE); angeryBusImageView.setVisibility(View.INVISIBLE); } /* There's a nice method called notifyDataSetChanged() to avoid building the ListView * all over again. This method exists in a billion answers on Stack Overflow, but * it's nowhere to be seen around here, Android Studio can't find it no matter what. * Anyway, it only works from Android 2.3 onward (which is why it refuses to appear, I * guess) and requires to modify the list with .add() and .clear() and some other * methods, so to update a single stop we need to completely rebuild the list for no * reason. It would probably end up as "slow" as throwing away the old ListView and * redrwaing everything. */ // Show results favoriteRecyclerView.setAdapter(new StopRecyclerAdapter(busStops,adapterListener)); } public void showBusStopUsernameInputDialog(final Stop busStop) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); LayoutInflater inflater = this.getLayoutInflater(); View renameDialogLayout = inflater.inflate(R.layout.rename_dialog, null); busStopNameText = (EditText) renameDialogLayout.findViewById(R.id.rename_dialog_bus_stop_name); busStopNameText.setText(busStop.getStopDisplayName()); busStopNameText.setHint(busStop.getStopDefaultName()); builder.setTitle(getString(R.string.dialog_rename_bus_stop_username_title)); builder.setView(renameDialogLayout); - builder.setPositiveButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String busStopUsername = busStopNameText.getText().toString(); - String oldUserName = busStop.getStopUserName(); - - // changed to none - if(busStopUsername.length() == 0) { - // unless it was already empty, set new - if(oldUserName != null) { - busStop.setStopUserName(null); - - } - } else { // changed to something - // something different? - if(!busStopUsername.equals(oldUserName)) { - busStop.setStopUserName(busStopUsername); - - } + builder.setPositiveButton(getString(android.R.string.ok), (dialog, which) -> { + String busStopUsername = busStopNameText.getText().toString(); + String oldUserName = busStop.getStopUserName(); + + // changed to none + if(busStopUsername.length() == 0) { + // unless it was already empty, set new + if(oldUserName != null) { + busStop.setStopUserName(null); + + } + } else { // changed to something + // something different? + if(!busStopUsername.equals(oldUserName)) { + busStop.setStopUserName(busStopUsername); + } - launchUpdate(busStop); - } - }); - builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); } + launchUpdate(busStop); }); - builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // delete user name from database - busStop.setStopUserName(null); - launchUpdate(busStop); + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); + builder.setNeutralButton(R.string.dialog_rename_bus_stop_username_reset_button, (dialog, which) -> { + // delete user name from database + busStop.setStopUserName(null); + launchUpdate(busStop); - } }); builder.show(); } private void launchUpdate(Stop busStop){ if (getContext()!=null) new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.UPDATE, - new AsyncStopFavoriteAction.ResultListener() { - @Override - public void doStuffWithResult(Boolean result) { - //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); - } + result -> { + //Toast.makeText(getApplicationContext(), R.string.tip_add_favorite, Toast.LENGTH_SHORT).show(); }).execute(busStop); } } diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java index d7d3a68..71f227e 100644 --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -1,740 +1,752 @@ package it.reyboz.bustorino.fragments; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.location.Criteria; import android.location.Location; import android.os.Build; import android.os.Bundle; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageButton; +import androidx.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.util.LocationCriteria; import it.reyboz.bustorino.util.Permissions; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSIONS; import static it.reyboz.bustorino.util.Permissions.LOCATION_PERMISSION_GIVEN; /** * A simple {@link Fragment} subclass. * Use the {@link MainScreenFragment#newInstance} factory method to * create an instance of this fragment. */ -public class MainScreenFragment extends BaseFragment implements FragmentListenerMain{ +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 final static String FRAGMENT_TAG = "MainScreenFragment"; /// UI ELEMENTS // private ImageButton addToFavorites; private FragmentHelper fragmentHelper; private SwipeRefreshLayout swipeRefreshLayout; private EditText busStopSearchByIDEditText; private EditText busStopSearchByNameEditText; private ProgressBar progressBar; private TextView howDoesItWorkTextView; private Button hideHintButton; private MenuItem actionHelpMenuItem; private FloatingActionButton floatingActionButton; private boolean setupOnAttached = true; private boolean suppressArrivalsReload = false; //private Snackbar snackbar; /* * Search mode */ private static final int SEARCH_BY_NAME = 0; private static final int SEARCH_BY_ID = 1; private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; private final ArrivalsFetcher[] arrivalsFetchers = 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(); } }; /// 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<String[]> requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { @Override public void onActivityResult(Map<String, Boolean> result) { if(result==null || result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null ||result.get(Manifest.permission.ACCESS_FINE_LOCATION) ) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) == null || result.get(Manifest.permission.ACCESS_FINE_LOCATION) == null) return; if(result.get(Manifest.permission.ACCESS_COARSE_LOCATION) && result.get(Manifest.permission.ACCESS_FINE_LOCATION)){ locationPermissionGranted = true; Log.w(DEBUG_TAG, "Starting position"); if (mListener!= null && getContext()!=null){ if (locationManager==null) locationManager = AppLocationManager.getInstance(getContext()); locationManager.addLocationRequestFor(requester); } } } }); //// ACTIVITY ATTACHED (LISTENER /// private CommonFragmentListener mListener; private String pendingStopID = null; + private CoordinatorLayout coordLayout; public MainScreenFragment() { // Required empty public constructor } public static MainScreenFragment newInstance() { MainScreenFragment fragment = new MainScreenFragment(); Bundle args = new Bundle(); //args.putString(ARG_PARAM1, param1); //args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { //do nothing } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_main_screen, container, false); - addToFavorites = (ImageButton) root.findViewById(R.id.addToFavorites); + addToFavorites = root.findViewById(R.id.addToFavorites); busStopSearchByIDEditText = root.findViewById(R.id.busStopSearchByIDEditText); busStopSearchByNameEditText = root.findViewById(R.id.busStopSearchByNameEditText); progressBar = root.findViewById(R.id.progressBar); howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); swipeRefreshLayout = root.findViewById(R.id.listRefreshLayout); floatingActionButton = root.findViewById(R.id.floatingActionButton); busStopSearchByIDEditText.setSelectAllOnFocus(true); busStopSearchByIDEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); busStopSearchByNameEditText .setOnEditorActionListener((v, actionId, event) -> { // IME_ACTION_SEARCH alphabetical option if (actionId == EditorInfo.IME_ACTION_SEARCH) { onSearchClick(v); return true; } return false; }); swipeRefreshLayout .setOnRefreshListener(() -> mainHandler.post(refreshStop)); swipeRefreshLayout.setColorSchemeResources(R.color.blue_500, R.color.orange_500); + coordLayout = root.findViewById(R.id.coord_layout); + floatingActionButton.setOnClickListener((this::onToggleKeyboardLayout)); hideHintButton.setOnClickListener(this::onHideHint); AppCompatImageButton qrButton = root.findViewById(R.id.QRButton); qrButton.setOnClickListener(this::onQRButtonClick); AppCompatImageButton searchButton = root.findViewById(R.id.searchButton); searchButton.setOnClickListener(this::onSearchClick); // Fragment stuff fragMan = getChildFragmentManager(); fragMan.addOnBackStackChangedListener(() -> Log.d("BusTO Main Fragment", "BACK STACK CHANGED")); fragmentHelper = new FragmentHelper(this, getChildFragmentManager(), getContext(), R.id.resultFrame); setSearchModeBusStopID(); cr.setAccuracy(Criteria.ACCURACY_FINE); cr.setAltitudeRequired(false); cr.setBearingRequired(false); cr.setCostAllowed(true); cr.setPowerRequirement(Criteria.NO_REQUIREMENT); - locationManager = AppLocationManager.getInstance(getContext()); + locationManager = AppLocationManager.getInstance(getContext()); Log.d(DEBUG_TAG, "OnCreateView, savedInstanceState null: "+(savedInstanceState==null)); return root; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Log.d(DEBUG_TAG, "onViewCreated, SwipeRefreshLayout visible: "+(swipeRefreshLayout.getVisibility()==View.VISIBLE)); Log.d(DEBUG_TAG, "Setup on attached: "+setupOnAttached); //Restore instance state if (savedInstanceState!=null){ Fragment fragment = getChildFragmentManager().getFragment(savedInstanceState, SAVED_FRAGMENT); if (fragment!=null){ getChildFragmentManager().beginTransaction().add(R.id.resultFrame, fragment).commit(); setupOnAttached = false; } } if (getChildFragmentManager().findFragmentById(R.id.resultFrame)!= null){ swipeRefreshLayout.setVisibility(View.VISIBLE); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment!=null) getChildFragmentManager().putFragment(outState, SAVED_FRAGMENT, fragment); fragmentHelper.setBlockAllActivities(true); } public void setSuppressArrivalsReload(boolean value){ suppressArrivalsReload = value; // we have to suppress the reloading of the (possible) ArrivalsFragment /*if(value) { Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment) { ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } } */ } + /** * Cancel the reload of the arrival times * because we are going to pop the fragment */ public void cancelReloadArrivalsIfNeeded(){ if(getContext()==null) return; //we are not attached //Fragment fr = getChildFragmentManager().findFragmentById(R.id.resultFrame); fragmentHelper.stopLastRequestIfNeeded(true); toggleSpinner(false); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { mListener = (CommonFragmentListener) context; } else { throw new RuntimeException(context + " must implement CommonFragmentListener"); } if (setupOnAttached) { if (pendingStopID==null) //We want the nearby bus stops! mainHandler.post(new NearbyStopsRequester(context, cr)); else{ ///TODO: if there is a stop displayed, we need to hold the update } setupOnAttached = false; } } @Override public void onDetach() { super.onDetach(); mListener = null; // setupOnAttached = true; } @Override public void onResume() { final Context con = getContext(); Log.w(DEBUG_TAG, "OnResume called"); if (con != null) { if(locationManager==null) locationManager = AppLocationManager.getInstance(con); if(Permissions.locationPermissionGranted(con)){ Log.d(DEBUG_TAG, "Location permission OK"); if(!locationManager.isRequesterRegistered(requester)) locationManager.addLocationRequestFor(requester); } else if(shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)){ //we have already asked for the location, and we should show an explanation in order // to ask again (TODO) //do nothing } else{ //request permission requestPermissionLauncher.launch(Permissions.LOCATION_PERMISSIONS); } } else { Log.w(DEBUG_TAG, "Context is null at onResume"); } super.onResume(); // if we have a pending stopID request, do it Log.d(DEBUG_TAG, "Pending stop ID for arrivals: "+pendingStopID); //this is the second time we are attaching this fragment Log.d(DEBUG_TAG, "Waiting for new stop request: "+ suppressArrivalsReload); if (suppressArrivalsReload){ // we have to suppress the reloading of the (possible) ArrivalsFragment Fragment fragment = getChildFragmentManager().findFragmentById(R.id.resultFrame); if (fragment instanceof ArrivalsFragment){ ArrivalsFragment frag = (ArrivalsFragment) fragment; frag.setReloadOnResume(false); } suppressArrivalsReload = false; } if(pendingStopID!=null){ requestArrivalsForStopID(pendingStopID); pendingStopID = null; } mListener.readyGUIfor(FragmentKind.MAIN_SCREEN_FRAGMENT); fragmentHelper.setBlockAllActivities(false); } @Override public void onPause() { //mainHandler = null; locationManager.removeLocationRequestFor(requester); super.onPause(); fragmentHelper.setBlockAllActivities(true); fragmentHelper.stopLastRequestIfNeeded(true); } /* GUI METHODS */ /** * QR scan button clicked * * @param v View QRButton clicked */ public void onQRButtonClick(View v) { IntentIntegrator integrator = new IntentIntegrator(getActivity()); integrator.initiateScan(); } public void onHideHint(View v) { hideHints(); setOption(OPTION_SHOW_LEGEND, false); } /** * OK this is pure shit * * @param v View clicked */ public void onSearchClick(View v) { final StopsFinderByName[] stopsFinderByNames = new StopsFinderByName[]{new GTTStopsFetcher(), new FiveTStopsFetcher()}; if (searchMode == SEARCH_BY_ID) { String busStopID = busStopSearchByIDEditText.getText().toString(); fragmentHelper.stopLastRequestIfNeeded(true); requestArrivalsForStopID(busStopID); } else { // searchMode == SEARCH_BY_NAME String query = busStopSearchByNameEditText.getText().toString(); query = query.trim(); if(getContext()!=null) { if (query.length() < 1) { Toast.makeText(getContext(), R.string.insert_bus_stop_name_error, Toast.LENGTH_SHORT).show(); } else if(query.length()< 3){ Toast.makeText(getContext(), R.string.query_too_short, Toast.LENGTH_SHORT).show(); } else { fragmentHelper.stopLastRequestIfNeeded(true); new AsyncStopsSearcher(fragmentHelper, stopsFinderByNames).execute(query); } } } } public void onToggleKeyboardLayout(View v) { if (searchMode == SEARCH_BY_NAME) { setSearchModeBusStopID(); if (busStopSearchByIDEditText.requestFocus()) { showKeyboard(); } } else { // searchMode == SEARCH_BY_ID setSearchModeBusStopName(); if (busStopSearchByNameEditText.requestFocus()) { showKeyboard(); } } } @Override public void enableRefreshLayout(boolean yes) { swipeRefreshLayout.setEnabled(yes); } ////////////////////////////////////// GUI HELPERS ///////////////////////////////////////////// public void showKeyboard() { if(getActivity() == null) return; InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = searchMode == SEARCH_BY_ID ? busStopSearchByIDEditText : busStopSearchByNameEditText; imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } private void setSearchModeBusStopID() { searchMode = SEARCH_BY_ID; busStopSearchByNameEditText.setVisibility(View.GONE); busStopSearchByNameEditText.setText(""); busStopSearchByIDEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.alphabetical); } private void setSearchModeBusStopName() { searchMode = SEARCH_BY_NAME; busStopSearchByIDEditText.setVisibility(View.GONE); busStopSearchByIDEditText.setText(""); busStopSearchByNameEditText.setVisibility(View.VISIBLE); floatingActionButton.setImageResource(R.drawable.numeric); } protected boolean isNearbyFragmentShown(){ Fragment fragment = getChildFragmentManager().findFragmentByTag(NearbyStopsFragment.FRAGMENT_TAG); return (fragment!= null && fragment.isVisible()); } /** * Having that cursor at the left of the edit text makes me cancer. * * @param busStopID bus stop ID */ private void setBusStopSearchByIDEditText(String busStopID) { busStopSearchByIDEditText.setText(busStopID); busStopSearchByIDEditText.setSelection(busStopID.length()); } private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } + @Nullable + @org.jetbrains.annotations.Nullable + @Override + public View getBaseViewForSnackBar() { + return coordLayout; + } + @Override public void toggleSpinner(boolean enable) { if (enable) { //already set by the RefreshListener when needed //swipeRefreshLayout.setRefreshing(true); progressBar.setVisibility(View.VISIBLE); } else { swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } } private void prepareGUIForBusLines() { swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(true); } private void prepareGUIForBusStops() { swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } void showNearbyStopsFragment(){ swipeRefreshLayout.setVisibility(View.VISIBLE); final Fragment existingFrag = fragMan.findFragmentById(R.id.resultFrame); NearbyStopsFragment fragment; if (!(existingFrag instanceof NearbyStopsFragment)){ //there is no fragment showing fragment = NearbyStopsFragment.newInstance(NearbyStopsFragment.TYPE_STOPS); FragmentTransaction ft = fragMan.beginTransaction(); //if (oldFrag != null) // ft.remove(oldFrag); ft.replace(R.id.resultFrame, fragment, NearbyStopsFragment.FRAGMENT_TAG); ft.commit(); } } @Override public void showFloatingActionButton(boolean yes) { mListener.showFloatingActionButton(yes); } /** * This provides a temporary fix to make the transition * to a single asynctask go smoother * * @param fragmentType the type of fragment created */ @Override public void readyGUIfor(FragmentKind fragmentType) { hideKeyboard(); //if we are getting results, already, stop waiting for nearbyStops if (pendingNearbyStopsRequest && (fragmentType == FragmentKind.ARRIVALS || fragmentType == FragmentKind.STOPS)) { locationManager.removeLocationRequestFor(requester); pendingNearbyStopsRequest = false; } if (fragmentType == null) Log.e("ActivityMain", "Problem with fragmentType"); else switch (fragmentType) { case ARRIVALS: prepareGUIForBusLines(); if (getOption(OPTION_SHOW_LEGEND, true)) { showHints(); } break; case STOPS: prepareGUIForBusStops(); break; default: Log.d(DEBUG_TAG, "Fragment type is unknown"); return; } // Shows hints } @Override public void showMapCenteredOnStop(Stop stop) { if(mListener!=null) mListener.showMapCenteredOnStop(stop); } /** * Main method for stops requests * @param ID the Stop ID */ @Override public void requestArrivalsForStopID(String ID) { if (!isResumed()){ //defer request pendingStopID = ID; Log.d(DEBUG_TAG, "Deferring update for stop "+ID); return; } final boolean delayedRequest = !(pendingStopID==null); final FragmentManager framan = getChildFragmentManager(); if (getContext()==null){ Log.e(DEBUG_TAG, "Asked for arrivals with null context"); return; } if (ID == null || ID.length() <= 0) { // we're still in UI thread, no need to mess with Progress showToastMessage(R.string.insert_bus_stop_number_error, true); toggleSpinner(false); } else if (framan.findFragmentById(R.id.resultFrame) instanceof ArrivalsFragment) { ArrivalsFragment fragment = (ArrivalsFragment) framan.findFragmentById(R.id.resultFrame); if (fragment != null && fragment.getStopID() != null && fragment.getStopID().equals(ID)){ // Run with previous fetchers //fragment.getCurrentFetchers().toArray() new AsyncArrivalsSearcher(fragmentHelper,fragment.getCurrentFetchersAsArray(), getContext()).execute(ID); } else{ new AsyncArrivalsSearcher(fragmentHelper, arrivalsFetchers, getContext()).execute(ID); } } else { new AsyncArrivalsSearcher(fragmentHelper,arrivalsFetchers, getContext()).execute(ID); Log.d(DEBUG_TAG, "Started search for arrivals of stop " + ID); } } /////////// LOCATION METHODS ////////// /* private void startStopRequest(String provider) { Log.d(DEBUG_TAG, "Provider " + provider + " got enabled"); if (locmgr != null && mainHandler != null && pendingNearbyStopsRequest && locmgr.getProvider(provider).meetsCriteria(cr)) { } } */ /** * Run location requests separately and asynchronously */ class NearbyStopsRequester implements Runnable { Context appContext; Criteria cr; public NearbyStopsRequester(Context appContext, Criteria criteria) { this.appContext = appContext.getApplicationContext(); this.cr = criteria; } @Override public void run() { if(isNearbyFragmentShown()) { //nothing to do Log.w(DEBUG_TAG, "launched nearby fragment request but we already are showing"); return; } final boolean isOldVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M; final boolean noPermission = ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED; //if we don't have the permission, we have to ask for it, if we haven't // asked too many times before if (noPermission) { if (!isOldVersion) { pendingNearbyStopsRequest = true; //Permissions.assertLocationPermissions(appContext,getActivity()); requestPermissionLauncher.launch(LOCATION_PERMISSIONS); Log.w(DEBUG_TAG, "Cannot get position: Asking permission, noPositionFromSys: " + noPermission); return; } else { Toast.makeText(appContext, "Asked for permission position too many times", Toast.LENGTH_LONG).show(); } } else setOption(LOCATION_PERMISSION_GIVEN, true); AppLocationManager appLocationManager = AppLocationManager.getInstance(appContext); final boolean haveProviders = appLocationManager.anyLocationProviderMatchesCriteria(cr); if (haveProviders && fragmentHelper.getLastSuccessfullySearchedBusStop() == null && !fragMan.isDestroyed()) { //Go ahead with the request Log.d("mainActivity", "Recreating stop fragment"); showNearbyStopsFragment(); pendingNearbyStopsRequest = false; } else if(!haveProviders){ Log.e(DEBUG_TAG, "NO PROVIDERS FOR POSITION"); } } } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/MapFragment.java b/src/it/reyboz/bustorino/fragments/MapFragment.java index 81fc88e..13b1cae 100644 --- a/src/it/reyboz/bustorino/fragments/MapFragment.java +++ b/src/it/reyboz/bustorino/fragments/MapFragment.java @@ -1,640 +1,644 @@ /* 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 <http://www.gnu.org/licenses/>. */ 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 BaseFragment { +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<String> 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<String[]> positionRequestLauncher = - registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback<Map<String, Boolean>>() { - @Override - @SuppressLint("MissingPermission") - public void onActivityResult(Map<String, Boolean> 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); - 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); - } + 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"); } + 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<Stop> stops){ if (getContext() == 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<AsyncStopFetcher.BoundingBoxLimit,Void, List<Stop>>{ final WeakReference<MapFragment> fragmentWeakReference; public AsyncStopFetcher(MapFragment fragment) { this.fragmentWeakReference = new WeakReference<>(fragment); } @Override protected List<Stop> 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, limit.longFrom, limit.latitTo); dbHelper.close(); return Arrays.asList(stops); } @Override protected void onPostExecute(List<Stop> 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()); 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; } } } } diff --git a/src/it/reyboz/bustorino/fragments/BaseFragment.java b/src/it/reyboz/bustorino/fragments/ScreenBaseFragment.java similarity index 81% rename from src/it/reyboz/bustorino/fragments/BaseFragment.java rename to src/it/reyboz/bustorino/fragments/ScreenBaseFragment.java index c1359ab..e8e6d86 100644 --- a/src/it/reyboz/bustorino/fragments/BaseFragment.java +++ b/src/it/reyboz/bustorino/fragments/ScreenBaseFragment.java @@ -1,45 +1,55 @@ package it.reyboz.bustorino.fragments; import android.content.Context; import android.content.SharedPreferences; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.BuildConfig; import static android.content.Context.MODE_PRIVATE; -public abstract class BaseFragment extends Fragment { +public abstract class ScreenBaseFragment extends Fragment { protected String PREF_FILE= BuildConfig.APPLICATION_ID+".fragment_prefs"; protected void setOption(String optionName, boolean value) { Context mContext = getContext(); SharedPreferences.Editor editor = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE).edit(); editor.putBoolean(optionName, value); editor.commit(); } protected boolean getOption(String optionName, boolean optDefault) { Context mContext = getContext(); SharedPreferences preferences = mContext.getSharedPreferences(PREF_FILE, MODE_PRIVATE); return preferences.getBoolean(optionName, optDefault); } protected void showToastMessage(int messageID, boolean short_lenght) { final int length = short_lenght ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG; Toast.makeText(getContext(), messageID, length).show(); } public void hideKeyboard() { + if (getActivity()==null) return; View view = getActivity().getCurrentFocus(); if (view != null) { ((InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)) .hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } + + /** + * Find the view on which the snackbar should be shown + * @return + */ + @Nullable + public abstract View getBaseViewForSnackBar(); } diff --git a/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java b/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java index 4f65a23..ed60ed8 100644 --- a/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java +++ b/src/it/reyboz/bustorino/middleware/AsyncStopsSearcher.java @@ -1,131 +1,131 @@ /* BusTO (middleware) Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.reyboz.bustorino.middleware; import android.os.AsyncTask; import android.util.Log; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.backend.StopsFinderByName; import it.reyboz.bustorino.fragments.FragmentHelper; public class AsyncStopsSearcher extends AsyncTask<String, Fetcher.Result, List<Stop>> { private static final String TAG = "BusTO-StopsSearcher"; private static final String DEBUG_TAG = TAG; private final StopsFinderByName[] fetchers; private final AtomicReference<Fetcher.Result> res; private WeakReference<FragmentHelper> helperWR; private String theQuery; public AsyncStopsSearcher(FragmentHelper fh, StopsFinderByName[] fetchers) { this.fetchers = fetchers; if (fetchers.length < 1){ throw new IllegalArgumentException("You have to put at least one Fetcher, idiot!"); } this.res = new AtomicReference<>(); this.helperWR = new WeakReference<>(fh); fh.setLastTaskRef(this); } @Override protected List<Stop> doInBackground(String... strings) { RecursionHelper<StopsFinderByName> r = new RecursionHelper<>(fetchers); if (helperWR.get()==null || strings.length == 0) return null; Log.d(DEBUG_TAG,"Running with query "+strings[0]); ArrayList<Fetcher.Result> results = new ArrayList<>(); List<Stop> resultsList; while (r.valid()){ if (this.isCancelled()) return null; final StopsFinderByName finder = r.getAndMoveForward(); theQuery = strings[0].trim(); resultsList = finder.FindByName(theQuery, res); Log.d(DEBUG_TAG, "Result: "+res.get()+", "+resultsList.size()+" stops"); if (res.get()== Fetcher.Result.OK){ return resultsList; } results.add(res.get()); } boolean emptyResults = true; for (Fetcher.Result re: results){ if (!re.equals(Fetcher.Result.EMPTY_RESULT_SET)) { emptyResults = false; break; } } if(emptyResults){ publishProgress(Fetcher.Result.EMPTY_RESULT_SET); } return new ArrayList<>(); } @Override protected void onProgressUpdate(Fetcher.Result... values) { FragmentHelper fh = helperWR.get(); if (fh!=null) for (Fetcher.Result r : values){ fh.showErrorMessage(r, SearchRequestType.STOPS); } else { Log.w(TAG,"We had to show some progress but activity was destroyed"); } } @Override protected void onCancelled() { FragmentHelper fh = helperWR.get(); if (fh!=null) fh.toggleSpinner(false); } @Override protected void onPreExecute() { FragmentHelper fh = helperWR.get(); if (fh!=null) fh.toggleSpinner(true); } @Override protected void onPostExecute(List<Stop> stops) { final FragmentHelper fh = helperWR.get(); - if (stops==null || fh==null || theQuery==null) { + if (stops==null || stops.size() == 0 || fh==null || theQuery==null) { if (fh!=null) fh.toggleSpinner(false); cancel(true); return; } if(isCancelled()){ fh.toggleSpinner(false); return; } fh.createStopListFragment(stops, theQuery, true); } }