diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsAdapter.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsAdapter.java index d3046f56ea..b1bfcf3400 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsAdapter.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsAdapter.java @@ -19,8 +19,8 @@ import android.database.Cursor; import android.graphics.drawable.GradientDrawable; import android.view.View; -import android.widget.ImageView; import android.widget.HorizontalScrollView; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -37,6 +37,8 @@ /** * Adapter for starred stops list that displays arrival badges inline. + * A null value in the arrivals map indicates a fetch error for that stop, + * while an empty list means no upcoming arrivals. */ class StarredStopsAdapter extends SimpleCursorAdapter { @@ -104,22 +106,42 @@ public void bindView(View view, Context context, Cursor cursor) { HorizontalScrollView arrivalsScroll = view.findViewById(R.id.arrivals_scroll); LinearLayout arrivalsContainer = view.findViewById(R.id.arrivals_container); ProgressBar arrivalsLoading = view.findViewById(R.id.arrivals_loading); + TextView arrivalsError = view.findViewById(R.id.arrivals_error); if (mArrivalsData == null) { // Still loading arrivalsScroll.setVisibility(View.GONE); + arrivalsError.setVisibility(View.GONE); arrivalsLoading.setVisibility(View.VISIBLE); return; } arrivalsLoading.setVisibility(View.GONE); + + if (!mArrivalsData.containsKey(stopId)) { + arrivalsScroll.setVisibility(View.GONE); + arrivalsError.setVisibility(View.GONE); + return; + } + ArrayList arrivals = mArrivalsData.get(stopId); - if (arrivals == null || arrivals.isEmpty()) { + // Null value means fetch error + if (arrivals == null) { + arrivalsScroll.setVisibility(View.GONE); + arrivalsError.setVisibility(View.VISIBLE); + arrivalsError.setText(R.string.starred_stop_arrivals_error); + return; + } + + // Empty list means no upcoming arrivals + if (arrivals.isEmpty()) { arrivalsScroll.setVisibility(View.GONE); + arrivalsError.setVisibility(View.GONE); return; } + arrivalsError.setVisibility(View.GONE); arrivalsScroll.setVisibility(View.VISIBLE); arrivalsContainer.removeAllViews(); diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsArrivalsLoader.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsArrivalsLoader.java index 0061bb8599..8f58ad1dde 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsArrivalsLoader.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/StarredStopsArrivalsLoader.java @@ -26,11 +26,18 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import androidx.loader.content.AsyncTaskLoader; /** - * Loader that fetches arrival info for multiple starred stops in the background. + * Loader that fetches arrival info for multiple starred stops in parallel. + * Uses a null value in the result map to indicate a fetch error for a stop, + * distinguishing it from an empty list (no upcoming arrivals). */ public class StarredStopsArrivalsLoader extends AsyncTaskLoader>> { @@ -41,6 +48,8 @@ public class StarredStopsArrivalsLoader private static final int MAX_ARRIVALS_PER_STOP = 3; + private static final int THREAD_POOL_SIZE = 3; + private final String[] mStopIds; private HashMap> mResult; @@ -55,32 +64,29 @@ public HashMap> loadInBackground() { HashMap> result = new HashMap<>(); long now = System.currentTimeMillis(); + if (mStopIds.length == 0) { + return result; + } + + ExecutorService executor = Executors.newFixedThreadPool( + Math.min(THREAD_POOL_SIZE, mStopIds.length)); + List> futures = new ArrayList<>(); + for (String stopId : mStopIds) { - ArrayList arrivals = new ArrayList<>(); + futures.add(executor.submit(new FetchArrivalsTask(getContext(), stopId, now))); + } + + for (int i = 0; i < mStopIds.length; i++) { try { - ObaArrivalInfoResponse response = - ObaArrivalInfoRequest.newRequest(getContext(), stopId, MINUTES_AFTER) - .call(); - if (response != null && response.getCode() == ObaApi.OBA_OK - && response.getArrivalInfo() != null) { - arrivals = ArrivalInfoUtils.convertObaArrivalInfo( - getContext(), response.getArrivalInfo(), null, now, false); - Collections.sort(arrivals, new ArrivalInfoUtils.InfoComparator()); - if (arrivals.size() > MAX_ARRIVALS_PER_STOP) { - arrivals = new ArrayList<>( - arrivals.subList(0, MAX_ARRIVALS_PER_STOP)); - } - } else if (response != null) { - Log.w(TAG, "API error for stop " + stopId - + ": code=" + response.getCode()); - } else { - Log.w(TAG, "Null response for stop " + stopId); - } + StopArrivalsResult stopResult = futures.get(i).get(); + result.put(stopResult.stopId, stopResult.arrivals); } catch (Exception e) { - Log.w(TAG, "Failed to fetch arrivals for stop " + stopId, e); + Log.w(TAG, "Failed to get result for stop " + mStopIds[i], e); + result.put(mStopIds[i], null); } - result.put(stopId, arrivals); } + + executor.shutdown(); return result; } @@ -113,4 +119,62 @@ protected void onReset() { onStopLoading(); mResult = null; } + + private static class StopArrivalsResult { + + final String stopId; + + final ArrayList arrivals; + + StopArrivalsResult(String stopId, ArrayList arrivals) { + this.stopId = stopId; + this.arrivals = arrivals; + } + } + + private static class FetchArrivalsTask implements Callable { + + private final Context mContext; + + private final String mStopId; + + private final long mNow; + + FetchArrivalsTask(Context context, String stopId, long now) { + mContext = context; + mStopId = stopId; + mNow = now; + } + + @Override + public StopArrivalsResult call() { + ArrayList arrivals = new ArrayList<>(); + try { + ObaArrivalInfoResponse response = + ObaArrivalInfoRequest.newRequest(mContext, mStopId, MINUTES_AFTER) + .call(); + if (response != null && response.getCode() == ObaApi.OBA_OK + && response.getArrivalInfo() != null) { + arrivals = ArrivalInfoUtils.convertObaArrivalInfo( + mContext, response.getArrivalInfo(), null, mNow, false); + Collections.sort(arrivals, new ArrivalInfoUtils.InfoComparator()); + if (arrivals.size() > MAX_ARRIVALS_PER_STOP) { + arrivals = new ArrayList<>( + arrivals.subList(0, MAX_ARRIVALS_PER_STOP)); + } + } else if (response != null) { + Log.w(TAG, "API error for stop " + mStopId + + ": code=" + response.getCode()); + return new StopArrivalsResult(mStopId, null); + } else { + Log.w(TAG, "Null response for stop " + mStopId); + return new StopArrivalsResult(mStopId, null); + } + } catch (Exception e) { + Log.w(TAG, "Failed to fetch arrivals for stop " + mStopId, e); + return new StopArrivalsResult(mStopId, null); + } + return new StopArrivalsResult(mStopId, arrivals); + } + } } diff --git a/onebusaway-android/src/main/res/layout/starred_stop_list_item.xml b/onebusaway-android/src/main/res/layout/starred_stop_list_item.xml index 616233a70e..15403f3d67 100644 --- a/onebusaway-android/src/main/res/layout/starred_stop_list_item.xml +++ b/onebusaway-android/src/main/res/layout/starred_stop_list_item.xml @@ -15,70 +15,44 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + - + + - + - + android:orientation="horizontal"/> + - - - + - - - + + diff --git a/onebusaway-android/src/main/res/values/strings.xml b/onebusaway-android/src/main/res/values/strings.xml index d61bc3c73b..1eb4c4651b 100644 --- a/onebusaway-android/src/main/res/values/strings.xml +++ b/onebusaway-android/src/main/res/values/strings.xml @@ -372,6 +372,7 @@ NOW %1$d min %1$s - %2$s + Could not load arrivals You have no starred routes.\n\nYou can add a star to a route on the arrivals screen by tapping on the star next to the route number.