// Copyright (C) 2010 Aleksandr Dobkin, Michael Choi, and Christopher Mills. // // This file is part of BusRadar <https://github.com/orgs/busradar/>. // // BusRadar 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. // // BusRadar 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. package busradar.madison; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.SocketException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Locale; import java.util.Scanner; import java.util.regex.*; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.conn.ssl.SSLSocketFactory; import android.app.Dialog; import android.content.Context; import android.database.Cursor; import android.graphics.Typeface; import android.text.Html; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; import android.widget.TextView; import android.widget.Toast; import android.view.Gravity; import static busradar.madison.G.dp2px; import static busradar.madison.ProblemReporter.*; public final class StopDialog extends Dialog { // static javax.net.ssl.SSLSocketFactory sslsockfactory; // // static { // // Imports: javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager // try { // // Create a trust manager that does not validate certificate chains // final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { // public void checkClientTrusted( final X509Certificate[] chain, final String authType ) { // } // public void checkServerTrusted( final X509Certificate[] chain, final String authType ) { // } // public X509Certificate[] getAcceptedIssuers() { // return null; // } // } }; // // // Install the all-trusting trust manager // final SSLContext sslContext = SSLContext.getInstance( "TLS" ); // sslContext.init( null, trustAllCerts, new java.security.SecureRandom() ); // // Create an ssl socket factory with our all-trusting manager // sslsockfactory = sslContext.getSocketFactory(); // // // // } catch ( final Exception e ) { // e.printStackTrace(); // } // // } final static Pattern num_vehicles_re = Pattern.compile("Next *\\d* *Vehicles? Arrives? at:"); final static Pattern time_re = Pattern.compile("(\\d+:\\d+ [ap]m).*<a.*>(.*)<"); final static Pattern no_busses_re = Pattern.compile("No further buses scheduled for this stop"); //final static Pattern no_timepoints_re = Pattern.compile("No stop information is found with this time point\\."); final static int route_list_id = 1; final static int time_list_id = 2; final static int stop_num_id = 3; ListView list_view; TextView status_text; TextView cur_loading_text; BaseAdapter times_adapter; final static String TRANSITTRACKER_URL = "http://webwatch.cityofmadison.com/tmwebwatch/LiveADAArrivalTimes"; static class RouteURL { static final int LOADING = 0; static final int DONE = 1; static final int NO_MORE_TODAY = 2; static final int NO_TIMEPOINTS = 3; static final int NO_STOPS_UNKONWN = 4; static final int ERROR = 5; int route; String url; //String text; int status = 0; } static final class RouteTime implements Comparable<RouteTime> { int route; String dir; String time; Date date; public int compareTo(RouteTime another) { return date.compareTo(another.date); } } final class CellView extends RelativeLayout { TextView route_textview; TextView dir_textview; TextView time_textview; CellView(Context ctx) { super(ctx); setPadding(dp2px(5), 0, dp2px(5), 0); addView(route_textview = new TextView(ctx) { { setId(1); setTextColor(0xffffffff); setTypeface(Typeface.DEFAULT_BOLD); setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize() * 2f); } }, new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) { { addRule(RelativeLayout.ALIGN_PARENT_TOP); } }); addView(dir_textview = new TextView(ctx) { { setTextColor(0xffffffff); } }, new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) { { addRule(RelativeLayout.BELOW, 1); } }); addView(time_textview = new TextView(ctx) { { setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize() * 2f); setTextColor(0xffffffff); } }, new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) { { addRule(RelativeLayout.ALIGN_PARENT_RIGHT); } }); } } int stopid; RouteURL[] routes; ArrayList<RouteTime> times = new ArrayList<RouteTime>(); ArrayList<RouteTime> curr_times = times; RouteURL selected_route; StopDialog(final Context ctx, final int stopid, final int lat, final int lon) { super(ctx, android.R.style.Theme_DeviceDefault_Dialog_MinWidth); this.stopid = stopid; // getWindow().requestFeature(Window.FEATURE_LEFT_ICON); //getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); DB.StopInfo stop_info = DB.getStopInfo(stopid); final String name = stop_info.name; int stopno = stop_info.stopno; final String link_html = stopno == -1 ? "" : String.format( "[<a href='http://www.cityofmadison.com/metro/BusStopDepartures/StopID/%04d.pdf'>%04d</a>]", stopno, stopno); setTitle(name.replaceFirst("\\s*\\[([NSEW]B)?#?\\d*\\]\\s*$", " $1")); TextView title = (TextView) findViewById(android.R.id.title); title.setHorizontallyScrolling(true); title.setEllipsize(TextUtils.TruncateAt.MARQUEE); title.setSelected(true); title.setTextColor(0xffffffff); title.setMarqueeRepeatLimit(-1); routes = get_time_urls(StopDialog.this.stopid); // getWindow().setLayout(LayoutParams.FILL_PARENT, // LayoutParams.FILL_PARENT); setContentView(new RelativeLayout(ctx) {{ addView(new TextView(ctx) {{ setId(stop_num_id); setText(Html.fromHtml(link_html)); setPadding(0, 0, dp2px(5), 0); this.setMovementMethod(LinkMovementMethod.getInstance()); }}, new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) {{ addRule(ALIGN_PARENT_RIGHT); }}); addView(new ImageView(ctx) { boolean enabled; @Override public void setEnabled(boolean e) { enabled = e; setImageResource(e ? R.drawable.love_enabled : R.drawable.love_disabled); if (e) { G.favorites.add_favorite_stop(stopid, name, lat, lon); Toast.makeText(ctx, "Added stop to Favorites", Toast.LENGTH_SHORT).show(); } else { G.favorites.remove_favorite_stop(stopid); Toast.makeText(ctx, "Removed stop from Favorites", Toast.LENGTH_SHORT).show(); } } { enabled = G.favorites.is_stop_favorite(stopid); setImageResource(enabled ? R.drawable.love_enabled : R.drawable.love_disabled); setPadding(0, 0, dp2px(10), 0); setOnClickListener(new OnClickListener() { public void onClick(View v) { setEnabled(!enabled); } }); }}, new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) {{ addRule(LEFT_OF, stop_num_id); setMargins(0, 0, 0, dp2px(3)); }}); addView(cur_loading_text = new TextView(ctx) {{ setText("Loading..."); setPadding(dp2px(5), 0, 0, 0); }}); addView(new HorizontalScrollView(ctx) { { setId(route_list_id); setHorizontalScrollBarEnabled(false); LinearLayout route_bar; addView(route_bar=new LinearLayout(ctx) { float text_size; Button cur_button; { setBaselineAligned(false); int last_route = -1; for (int i = 0; i < routes.length; i++) { final RouteURL route = routes[i]; if (route.route == last_route) continue; last_route = route.route; addView(new Button(ctx) { public void setEnabled(boolean e) { if (e) { setBackgroundColor(0xff000000 | G.routes[route.route].color); setTextSize(TypedValue.COMPLEX_UNIT_PX, text_size * 1.5f); } else { setBackgroundColor(0x90000000 | G.routes[route.route].color); setTextSize(TypedValue.COMPLEX_UNIT_PX, text_size); } } { setText(G.routes[route.route].name); setTextColor(0xffffffff); setTypeface(Typeface.DEFAULT_BOLD); text_size = getTextSize(); if (G.active_route == route.route) { setEnabled(true); cur_button = this; } else setEnabled(false); final Button b = this; setOnClickListener(new OnClickListener() { public void onClick(View v) { if (cur_button != null) { cur_button.setEnabled(false); } if (cur_button == b) { cur_button.setEnabled(false); cur_button = null; selected_route = null; update_time_display(); } else { cur_button = b; cur_button.setEnabled(true); selected_route = route; update_time_display(); } } }); } }, new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) {{ gravity = Gravity.BOTTOM; }}); } } }); route_bar.measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); route_bar.setMinimumHeight((int) (route_bar.getMeasuredHeight() * 1.5)); } }, new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) { { addRule(RelativeLayout.BELOW, stop_num_id); addRule(RelativeLayout.CENTER_HORIZONTAL); } }); addView(status_text = new TextView(ctx) {{ setText(""); }}, new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) {{ addRule(RelativeLayout.BELOW, route_list_id); addRule(RelativeLayout.CENTER_HORIZONTAL); }}); addView(list_view = new ListView(ctx) { { setId(time_list_id); setVerticalScrollBarEnabled(false); setAdapter(times_adapter = new BaseAdapter() { public View getView(final int position, View convertView, ViewGroup parent) { CellView v; if (convertView == null) v = new CellView(ctx); else v = (CellView) convertView; RouteTime rt = curr_times.get(position); v.setBackgroundColor(G.routes[rt.route].color | 0xff000000); v.route_textview.setText(G.routes[rt.route].name); if (rt.dir != null) v.dir_textview.setText("to "+ rt.dir); v.time_textview.setText(rt.time); return v; } public int getCount() { return curr_times.size(); } public Object getItem(int position) { return null; } public long getItemId(int position) { return 0; } }); } }, new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT) {{ addRule(RelativeLayout.BELOW, route_list_id); }}); }}); // getWindow().set, value) // getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, // android.R.drawable.ic_dialog_info); // title.setGravity(Gravity.CENTER_HORIZONTAL|Gravity.CENTER_VERTICAL); if (G.active_route >= 0) for (int i = 0; i < routes.length; i++) if (routes[i].route == G.active_route) { selected_route = routes[i]; RouteURL[] rnew = new RouteURL[routes.length]; rnew[0] = selected_route; for (int j = 0, k = 1; j < routes.length; j++) if (j != i) rnew[k++] = routes[j]; routes = rnew; break; } update_time_display(); } @Override public void show() { new Thread() { @Override public void run() { for (final RouteURL r : routes) { G.activity.runOnUiThread(new Runnable() { public void run() { cur_loading_text.setText(String.format("Loading route %s...", G.routes[r.route].name)); } }); final ArrayList<RouteTime> curtimes = new ArrayList<RouteTime>(); try { String data = util.http_get(TRANSITTRACKER_URL+r.url); System.err.printf("BusRadar URL %s: %s\n", TRANSITTRACKER_URL+r.url, data); //String outstr_cur = "Route " + r.route + "\n"; //scan.findWithinHorizon("(.*)", 0); //System.out.printf("BusRadar: %s\n", scan.nextLine()); if (num_vehicles_re.matcher(data).find()) { //System.out.printf("BusRadar: found num vehicles re\n"); Matcher m = time_re.matcher(data); while (m.find()) { RouteTime time = new RouteTime(); time.route = r.route; time.time = m.group(1).replace(".", ""); time.dir = m.group(2); //time.date = DateFormat.getTimeInstance(DateFormat.SHORT).parse(time.time); SimpleDateFormat f = new SimpleDateFormat("h:mm aa", Locale.US); time.date = f.parse(time.time); r.status = RouteURL.DONE; //outstr_cur += String.format("%s to %s\n", time.time, time.dir); curtimes.add(time); } } else if (no_busses_re.matcher(data).find()) { r.status = RouteURL.NO_MORE_TODAY; } // else if (scan.findWithinHorizon(no_timepoints_re, 0) != null) { // r.status = RouteURL.NO_TIMEPOINTS; // } // else { // r.status = RouteURL.ERROR; // System.out.printf("BusRadar: Could not get stop info for %s\n", r.url); // // throw new Exception("Error parsing TransitTracker webpage."); // }0 else { r.status = RouteURL.NO_STOPS_UNKONWN; } //r.text = outstr_cur; G.activity.runOnUiThread(new Runnable() { public void run() { times.addAll(curtimes); StopDialog.this.update_times(); } }); } // catch (final IOException ioe) { // log_problem(ioe); // G.activity.runOnUiThread(new Runnable() { // public void run() { // final Context ctx = StopDialog.this.getContext(); // // StopDialog.this.setContentView(new RelativeLayout(ctx) {{ // addView(new TextView(ctx) {{ // setText(Html.fromHtml("Error downloading data. Is the data connection enabled?"+ // "<p>Report problems to <a href='mailto:support@busradarapp.com'>support@busradarapp.com</a><p>"+ioe)); // setPadding(5, 5, 5, 5); // this.setMovementMethod(LinkMovementMethod.getInstance()); // }}, new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); // }}); // } // }); // return; // } catch (Exception e) { log_problem(e); String custom_msg = ""; final String turl = TRANSITTRACKER_URL+r.url; if ((e instanceof SocketException) || (e instanceof UnknownHostException)) { // data connection doesn't work custom_msg = "Error downloading data. Is the data connection enabled?"+ "<p>Report problems to <a href='mailto:support@busradarapp.com'>support@busradarapp.com</a><p>"+TextUtils.htmlEncode(e.toString()); } else { String rurl = String.format("http://www.cityofmadison.com/metro/BusStopDepartures/StopID/%04d.pdf", stopid); custom_msg = "Trouble retrieving real-time arrival estimates from <a href='"+turl+"'>this</a> TransitTracker webpage, which is displayed below. "+ "Meanwhile, try PDF timetable <a href='"+rurl+"'>here</a>. "+ "Contact us at <a href='mailto:support@busradarapp.com'>support@busradarapp.com</a> to report the problem.<p>"+ TextUtils.htmlEncode(e.toString()); } final String msg = custom_msg; G.activity.runOnUiThread(new Runnable() { public void run() { final Context ctx = StopDialog.this.getContext(); StopDialog.this.setContentView(new RelativeLayout(ctx) {{ addView(new TextView(ctx) {{ setId(1); setText(Html.fromHtml(msg)); setPadding(dp2px(5), dp2px(5), dp2px(5), dp2px(5)); this.setMovementMethod(LinkMovementMethod.getInstance()); }}, new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); addView(new WebView(ctx) {{ setWebViewClient(new WebViewClient()); loadUrl(turl); }}, new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT) {{ addRule(RelativeLayout.BELOW, 1); }}); }}); } }); return; } } G.activity.runOnUiThread(new Runnable() { public void run() { cur_loading_text.setText(""); if (times.isEmpty()) { status_text.setText("No further buses scheduled for this stop."); } } }); } }.start(); super.show(); } void update_times() { Collections.sort(times); update_time_display(); } void update_time_display() { if (selected_route != null) { switch (selected_route.status) { case RouteURL.DONE: status_text.setText(""); break; case RouteURL.LOADING: status_text.setText("Loading..."); break; case RouteURL.NO_MORE_TODAY: status_text.setText("No further buses scheduled for this stop."); break; case RouteURL.NO_TIMEPOINTS: status_text.setText("This route does not run today."); break; case RouteURL.NO_STOPS_UNKONWN: status_text.setText("No stops to show."); break; case RouteURL.ERROR: status_text.setText("Unkown error."); break; default: throw new Error(); } ArrayList<RouteTime> list = new ArrayList<RouteTime>(); for (RouteTime t : times) { if (t.route == selected_route.route) list.add(t); } curr_times = list; } else { curr_times = times; status_text.setText(""); } times_adapter.notifyDataSetChanged(); } static RouteURL[] get_time_urls(int stopid) { Cursor c = G.db.rawQuery("SELECT url, route " + "FROM routestops " + "WHERE stopid = ? " + "ORDER BY route" , new String[] { stopid + "" }); RouteURL[] list = new RouteURL[c.getCount()]; //ArrayList<RouteURL> list = new ArrayLst<RouteURL>(); //int last = -1; int i = 0; while (c.moveToNext()) { RouteURL r = new RouteURL(); r.url = c.getString(0); r.route = c.getInt(1); list[i++] = r; } c.close(); return list; } }