package org.frasermccrossan.ltc; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Timer; import java.util.TimerTask; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.content.res.Configuration; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; import android.widget.Toast; public class StopTimes extends Activity { // how long before popping up an Toast about how long the LTC site is taking static final long WARNING_DELAY = 10000; String stopNumber; LinearLayout routeViewLayout; ScrollView vertRouteViewScrollview; HorizontalScrollView horizRouteViewScrollview; Button refreshButton; Button notWorkingButton; ArrayList<LTCRoute> routeList; ArrayList<RouteDirTextView> routeViews; PredictionAdapter adapter; ListView predictionList; PredictionTask task = null; ArrayList<Prediction> predictions; OnClickListener buttonListener = new OnClickListener() { public void onClick(View v) { switch (v.getId()) { case R.id.refresh: getPredictions(); break; case R.id.not_working: Intent diagnoseIntent = new Intent(StopTimes.this, DiagnoseProblems.class); LTCScraper scraper = new LTCScraper(StopTimes.this); diagnoseIntent.putExtra("testurl", routeViews.get(0).getPredictionUrl(scraper, stopNumber)); startActivity(diagnoseIntent); break; // no default } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left); setContentView(R.layout.stop_times); BusDb db = new BusDb(this); Intent intent = getIntent(); stopNumber = intent.getStringExtra(BusDb.STOP_NUMBER); LTCStop stop = db.findStop(stopNumber); if (stop != null) { setTitle(stop.name); db.noteStopUse(stop.number); } refreshButton = (Button)findViewById(R.id.refresh); refreshButton.setOnClickListener(buttonListener); notWorkingButton = (Button)findViewById(R.id.not_working); notWorkingButton.setOnClickListener(buttonListener); routeViewLayout = (LinearLayout)findViewById(R.id.route_list); vertRouteViewScrollview = (ScrollView)findViewById(R.id.vert_route_list_scrollview); horizRouteViewScrollview = (HorizontalScrollView)findViewById(R.id.horiz_route_list_scrollview); String routeNumberOnly = intent.getStringExtra(BusDb.ROUTE_NUMBER); int routeDirectionOnly = intent.getIntExtra(BusDb.DIRECTION_NUMBER, 0); routeList = db.findStopRoutes(stopNumber, routeNumberOnly, routeDirectionOnly); db.close(); if (routeList.size() == 0) { Toast.makeText(StopTimes.this, R.string.none_stop_today, Toast.LENGTH_SHORT).show(); finish(); } else { /* now create a list of route views based on that routeList */ routeViews = new ArrayList<RouteDirTextView>(routeList.size()); for (LTCRoute route : routeList) { RouteDirTextView routeView = new RouteDirTextView(this, route); routeViews.add(routeView); routeViewLayout.addView(routeView); } predictionList = (ListView)findViewById(R.id.prediction_list); predictionList.setEmptyView(findViewById(R.id.empty_prediction_list)); predictions = new ArrayList<Prediction>(3); adapter = new PredictionAdapter(this, R.layout.prediction_item, predictions); predictionList.setAdapter(adapter); } } @Override protected void onStart () { super.onStart(); getPredictions(); } @Override protected void onPause() { super.onPause(); } @Override protected void onStop() { cancelTask(); super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); } void cancelTask() { if (task != null) { task.cancel(true); // we don't care if this fails because it has already stopped task = null; } } @SuppressLint("NewApi") void getPredictions() { cancelTask(); notWorkingButton.setVisibility(Button.GONE); task = new PredictionTask(); RouteDirTextView[] routeViewAry = new RouteDirTextView[routeViews.size()]; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { /* on Honeycomb and later, ASyncTasks run on a serial executor, and since * we might have another asynctask running in an activity (e.g. fetching stop lists), * we don't really want them all to block */ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (RouteDirTextView[])(routeViews.toArray(routeViewAry))); } else { task.execute((RouteDirTextView[])(routeViews.toArray(routeViewAry))); } } /* this takes one or more LTCRoute objects and fetches the predictions for each * one, publishing to the UI thread using publishProgress (somewhat counter-intuitively) * and finally returns a void result since the list will have been updated by then */ class PredictionTask extends AsyncTask<RouteDirTextView, RouteDirTextView, Void> { Toast toast; Timer toastTimer; int probCount = 0; class ToastTask extends TimerTask { public void run() { toast.show(); } } @SuppressLint("ShowToast") protected void onPreExecute() { toast = Toast.makeText(StopTimes.this, R.string.website_slow, Toast.LENGTH_LONG); toast.setGravity(Gravity.CENTER, 0, 0); scheduleTimer(); for (RouteDirTextView routeView: routeViews) { routeView.setStatus(RouteDirTextView.IDLE, null); routeView.updateDisplay(); } for (Prediction pred: predictions) { pred.setQuerying(); } } protected Void doInBackground(RouteDirTextView... routeViews) { LTCScraper scraper = new LTCScraper(StopTimes.this); for (RouteDirTextView routeView: routeViews) { routeView.setStatus(RouteDirTextView.QUERYING, null); publishProgress(routeView); routeView.scrapePredictions(scraper, stopNumber); if (isCancelled()) { break; } publishProgress(routeView); } return null; } protected void onProgressUpdate(RouteDirTextView... routeViews) { // update predictions and ping the listview cancelTimer(); scheduleTimer(); Calendar now = Calendar.getInstance(); for (RouteDirTextView routeView: routeViews) { if (isCancelled()) { break; } if (routeView.isOkToPost()) { removeRouteFromPredictions(routeView.route); for (Prediction p: routeView.getPredictions()) { // find the position where this Prediction should be inserted int insertPosition = Collections.binarySearch(predictions, p); // we don't care if we get a direct hit or just an insert position, we do the same thing if (insertPosition < 0) { insertPosition = -(insertPosition + 1); } // insert the Prediction at that location adapter.insert(p, insertPosition); } } switch (routeView.problemType) { case ScrapeStatus.PROBLEM_IMMEDIATELY: notWorkingButton.setVisibility(Button.VISIBLE); break; case ScrapeStatus.PROBLEM_IF_ALL: ++probCount; break; } for (Prediction p: predictions) { p.updateFields(StopTimes.this, now); } adapter.notifyDataSetChanged(); routeView.updateDisplay(); if (isCancelled()) { break; } if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT && horizRouteViewScrollview != null) { int right = routeView.getRight(); int svWidth = horizRouteViewScrollview.getWidth(); if (right > svWidth) { horizRouteViewScrollview.smoothScrollTo(right - svWidth, 0); } else { int left = routeView.getLeft(); int scrollPos = horizRouteViewScrollview.getScrollX(); if (left < scrollPos) { horizRouteViewScrollview.smoothScrollTo(left, 0); } } } else if (vertRouteViewScrollview != null) { int bottom = routeView.getBottom(); int svHeight = vertRouteViewScrollview.getHeight(); if (bottom > svHeight) { vertRouteViewScrollview.smoothScrollTo(0, bottom - svHeight); } else { int top = routeView.getTop(); int scrollPos = vertRouteViewScrollview.getScrollY(); if (top < scrollPos) { vertRouteViewScrollview.smoothScrollTo(top, 0); } } } } } @Override protected void onPostExecute(Void result) { if (probCount == routeViews.size()) { notWorkingButton.setVisibility(Button.VISIBLE); } cancelTimer(); } @Override protected void onCancelled() { cancelTimer(); } void scheduleTimer() { toastTimer = new Timer(); ToastTask toastTask = new ToastTask(); toastTimer.schedule(toastTask, WARNING_DELAY); } void cancelTimer() { toastTimer.cancel(); toast.cancel(); } // removes all references to a particular route from the prediction list private void removeRouteFromPredictions(LTCRoute route) { int i = 0; while (i < adapter.getCount()) { Prediction entry = predictions.get(i); if (entry.isOnRoute(route)) { adapter.remove(entry); } else { ++i; } } } } }