package org.mtransit.android.ui.fragment; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import org.mtransit.android.R; import org.mtransit.android.commons.BundleUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.TaskUtils; import org.mtransit.android.commons.ThreadSafeDateFormatter; import org.mtransit.android.commons.TimeUtils; import org.mtransit.android.commons.data.RouteTripStop; import org.mtransit.android.commons.data.Schedule; import org.mtransit.android.commons.data.Trip; import org.mtransit.android.commons.provider.POIProviderContract; import org.mtransit.android.commons.task.MTAsyncTask; import org.mtransit.android.commons.ui.widget.MTBaseAdapter; import org.mtransit.android.data.DataSourceManager; import org.mtransit.android.data.DataSourceProvider; import org.mtransit.android.data.POIManager; import org.mtransit.android.task.ScheduleTimestampsLoader; import org.mtransit.android.util.LoaderUtils; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.AbsListView; import android.widget.TextView; public class ScheduleDayFragment extends MTFragmentV4 implements VisibilityAwareFragment, DataSourceProvider.ModulesUpdateListener, LoaderManager.LoaderCallbacks<ArrayList<Schedule.Timestamp>> { private static final String TAG = ScheduleDayFragment.class.getSimpleName(); @Override public String getLogTag() { return TAG + "-" + dayStartsAtInMs; } private static final String EXTRA_AUTHORITY = "extra_agency_authority"; private static final String EXTRA_POI_UUID = "extra_poi_uuid"; private static final String EXTRA_DAY_START_AT_IN_MS = "extra_day_starts_at_ms"; private static final String EXTRA_FRAGMENT_POSITION = "extra_fragment_position"; private static final String EXTRA_LAST_VISIBLE_FRAGMENT_POSITION = "extra_last_visible_fragment_position"; private static final String EXTRA_SCOLLED_TO_NOW = "extra_scolled_to_now"; public static ScheduleDayFragment newInstance(String uuid, String authority, long dayStartsAtInMs, int fragmentPosition, int lastVisibleFragmentPosition, RouteTripStop optRts) { ScheduleDayFragment f = new ScheduleDayFragment(); Bundle args = new Bundle(); args.putString(EXTRA_AUTHORITY, authority); f.authority = authority; args.putString(EXTRA_POI_UUID, uuid); f.uuid = uuid; f.rts = optRts; args.putLong(EXTRA_DAY_START_AT_IN_MS, dayStartsAtInMs); f.dayStartsAtInMs = dayStartsAtInMs; if (fragmentPosition >= 0) { args.putInt(EXTRA_FRAGMENT_POSITION, fragmentPosition); f.fragmentPosition = fragmentPosition; } if (lastVisibleFragmentPosition >= 0) { args.putInt(EXTRA_LAST_VISIBLE_FRAGMENT_POSITION, lastVisibleFragmentPosition); f.lastVisibleFragmentPosition = lastVisibleFragmentPosition; } f.setArguments(args); return f; } private TimeAdapter adapter; private int fragmentPosition = -1; private int lastVisibleFragmentPosition = -1; private boolean fragmentVisible = false; private boolean scrolledToNow = false; private long dayStartsAtInMs = -1l; private Calendar dayStartsAtCal = null; private Date dayStartsAtDate; private void resetDayStarts() { this.dayStartsAtCal = null; this.dayStartsAtDate = null; } private Calendar getDayStartsAtCal() { if (this.dayStartsAtCal == null) { initDayStartsAtCal(); } return this.dayStartsAtCal; } private void initDayStartsAtCal() { if (this.dayStartsAtInMs > 0l) { this.dayStartsAtCal = TimeUtils.getNewCalendar(this.dayStartsAtInMs); } } private Date getDayStartsAtDate() { if (this.dayStartsAtDate == null) { initDayStartsAtDate(); } return this.dayStartsAtDate; } private void initDayStartsAtDate() { if (getDayStartsAtCal() != null) { this.dayStartsAtDate = getDayStartsAtCal().getTime(); } } private String authority; private String uuid; private RouteTripStop rts; private boolean hasRts() { if (this.rts == null) { initRtsAsync(); return false; } return true; } private void initRtsAsync() { if (this.loadRtsTask != null && this.loadRtsTask.getStatus() == MTAsyncTask.Status.RUNNING) { return; } if (TextUtils.isEmpty(this.uuid) || TextUtils.isEmpty(this.authority)) { return; } this.loadRtsTask = new LoadRtsTask(); TaskUtils.execute(this.loadRtsTask); } private LoadRtsTask loadRtsTask = null; private class LoadRtsTask extends MTAsyncTask<Void, Void, Boolean> { @Override public String getLogTag() { return ScheduleDayFragment.this.getLogTag() + ">" + LoadRtsTask.class.getSimpleName(); } @Override protected Boolean doInBackgroundMT(Void... params) { return initRtsSync(); } @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); if (result) { applyNewRts(); } } } private void resetRts() { this.rts = null; } private RouteTripStop getRtsOrNull() { if (!hasRts()) { return null; } return this.rts; } private boolean initRtsSync() { if (this.rts != null) { return false; } if (!TextUtils.isEmpty(this.uuid) && !TextUtils.isEmpty(this.authority)) { POIManager poim = DataSourceManager.findPOI(getActivity(), this.authority, POIProviderContract.Filter.getNewUUIDFilter(this.uuid)); if (poim != null && poim.poi instanceof RouteTripStop) { this.rts = (RouteTripStop) poim.poi; } } return this.rts != null; } private void applyNewRts() { if (this.rts == null) { return; } if (this.adapter != null) { this.adapter.setRts(this.rts); } if (this.adapter == null || !this.adapter.isInitialized()) { LoaderUtils.restartLoader(this, SCHEDULE_LOADER, null, this); } } @Override public void onAttach(Activity activity) { super.onAttach(activity); initAdapters(activity); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); restoreInstanceState(savedInstanceState, getArguments()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_schedule_day, container, false); setupView(view); return view; } @Override public void onSaveInstanceState(Bundle outState) { if (!TextUtils.isEmpty(this.authority)) { outState.putString(EXTRA_AUTHORITY, this.authority); } if (!TextUtils.isEmpty(this.uuid)) { outState.putString(EXTRA_POI_UUID, this.uuid); } if (this.dayStartsAtInMs >= 0l) { outState.putLong(EXTRA_DAY_START_AT_IN_MS, this.dayStartsAtInMs); } if (this.fragmentPosition >= 0) { outState.putInt(EXTRA_FRAGMENT_POSITION, this.fragmentPosition); } if (this.lastVisibleFragmentPosition >= 0) { outState.putInt(EXTRA_LAST_VISIBLE_FRAGMENT_POSITION, this.lastVisibleFragmentPosition); } outState.putBoolean(EXTRA_SCOLLED_TO_NOW, this.scrolledToNow); super.onSaveInstanceState(outState); } private void restoreInstanceState(Bundle... bundles) { String newAuthority = BundleUtils.getString(EXTRA_AUTHORITY, bundles); if (!TextUtils.isEmpty(newAuthority) && !newAuthority.equals(this.authority)) { this.authority = newAuthority; resetRts(); } String newUuid = BundleUtils.getString(EXTRA_POI_UUID, bundles); if (!TextUtils.isEmpty(newUuid) && !newUuid.equals(this.uuid)) { this.uuid = newUuid; resetRts(); } Long newDayStartsAtInMs = BundleUtils.getLong(EXTRA_DAY_START_AT_IN_MS, bundles); if (newDayStartsAtInMs != null && !newDayStartsAtInMs.equals(this.dayStartsAtInMs)) { this.dayStartsAtInMs = newDayStartsAtInMs; resetDayStarts(); } Integer newFragmentPosition = BundleUtils.getInt(EXTRA_FRAGMENT_POSITION, bundles); if (newFragmentPosition != null) { if (newFragmentPosition >= 0) { this.fragmentPosition = newFragmentPosition; } else { this.fragmentPosition = -1; } } Integer newLastVisibleFragmentPosition = BundleUtils.getInt(EXTRA_LAST_VISIBLE_FRAGMENT_POSITION, bundles); if (newLastVisibleFragmentPosition != null) { if (newLastVisibleFragmentPosition >= 0) { this.lastVisibleFragmentPosition = newLastVisibleFragmentPosition; } else { this.lastVisibleFragmentPosition = -1; } } Boolean newScrolledToNow = BundleUtils.getBoolean(EXTRA_SCOLLED_TO_NOW, bundles); if (newScrolledToNow != null) { this.scrolledToNow = newScrolledToNow; } this.adapter.setDayStartsAt(getDayStartsAtCal()); } public int getFragmentPosition() { return fragmentPosition; } private void setupView(View view) { if (view == null) { return; } ((TextView) view.findViewById(R.id.dayDate)).setText(getDayDateString()); setupAdapter(view); } private static final ThreadSafeDateFormatter DAY_DATE_FORMAT = new ThreadSafeDateFormatter("EEEE, MMM d, yyyy"); private CharSequence getDayDateString() { CharSequence dateS; if (TimeUtils.isYesterday(this.dayStartsAtInMs)) { dateS = getString(R.string.yesterday) + " " + DAY_DATE_FORMAT.formatThreadSafe(getDayStartsAtDate()); } else if (TimeUtils.isToday(this.dayStartsAtInMs)) { dateS = getString(R.string.today) + " " + DAY_DATE_FORMAT.formatThreadSafe(getDayStartsAtDate()); } else if (TimeUtils.isTomorrow(this.dayStartsAtInMs)) { dateS = getString(R.string.tomorrow) + " " + DAY_DATE_FORMAT.formatThreadSafe(getDayStartsAtDate()); } else { dateS = DAY_DATE_FORMAT.formatThreadSafe(getDayStartsAtDate()); } return dateS; } private void setupAdapter(View view) { if (view == null) { return; } inflateList(view); switchView(view); linkAdapterWithListView(view); } private void linkAdapterWithListView(View view) { if (view == null || this.adapter == null) { return; } View listView = view.findViewById(R.id.list); if (listView != null) { ((AbsListView) listView).setAdapter(this.adapter); } } @Override public void onModulesUpdated() { } @Override public void onPause() { super.onPause(); onFragmentInvisible(); } @Override public void onResume() { super.onResume(); if (this.fragmentPosition >= 0 && this.fragmentPosition == this.lastVisibleFragmentPosition) { onFragmentVisible(); } // ELSE would be call later if (this.adapter != null) { this.adapter.setActivity(getActivity()); } } @Override public void setFragmentPosition(int fragmentPosition) { this.fragmentPosition = fragmentPosition; setFragmentVisibleAtPosition(this.lastVisibleFragmentPosition); // force reset visibility } @Override public void setFragmentVisibleAtPosition(int visibleFragmentPosition) { if (this.lastVisibleFragmentPosition == visibleFragmentPosition // && (// (this.fragmentPosition == visibleFragmentPosition && this.fragmentVisible) // || // (this.fragmentPosition != visibleFragmentPosition && !this.fragmentVisible) // ) // ) { return; } this.lastVisibleFragmentPosition = visibleFragmentPosition; if (this.fragmentPosition < 0) { return; } if (this.fragmentPosition == visibleFragmentPosition) { onFragmentVisible(); } else { onFragmentInvisible(); } } private void onFragmentInvisible() { if (!this.fragmentVisible) { return; // already invisible } this.fragmentVisible = false; if (this.adapter != null) { this.adapter.onPause(); } } @Override public boolean isFragmentVisible() { return this.fragmentVisible; } private void onFragmentVisible() { if (this.fragmentVisible) { return; // already visible } if (!isResumed()) { return; } this.fragmentVisible = true; switchView(getView()); if (this.adapter == null || !this.adapter.isInitialized()) { if (hasRts()) { LoaderUtils.restartLoader(this, SCHEDULE_LOADER, null, this); } } else { this.adapter.onResume(getActivity()); } } private static final int SCHEDULE_LOADER = 0; @Override public Loader<ArrayList<Schedule.Timestamp>> onCreateLoader(int id, Bundle args) { switch (id) { case SCHEDULE_LOADER: RouteTripStop rts = getRtsOrNull(); if (this.dayStartsAtInMs <= 0l || rts == null) { return null; } return new ScheduleTimestampsLoader(getActivity(), rts, this.dayStartsAtInMs); default: MTLog.w(this, "Loader id '%s' unknown!", id); return null; } } @Override public void onLoaderReset(Loader<ArrayList<Schedule.Timestamp>> loader) { if (this.adapter != null) { this.adapter.clearTimes(); this.adapter.onPause(); } } @Override public void onLoadFinished(Loader<ArrayList<Schedule.Timestamp>> loader, ArrayList<Schedule.Timestamp> data) { View view = getView(); if (view == null) { return; // too late } switchView(view); this.adapter.setTimes(data); if (!this.scrolledToNow) { int compareToToday = this.adapter.compareToToday(); int selectPosition; if (compareToToday < 0) { // past selectPosition = this.adapter.getCount(); // scroll down } else if (compareToToday > 0) { // future selectPosition = 0; // scroll up } else { // today selectPosition = getTodaySelectPosition(); } ((AbsListView) view.findViewById(R.id.list)).setSelection(selectPosition); this.scrolledToNow = true; } switchView(view); } private int getTodaySelectPosition() { Schedule.Timestamp nextTime = this.adapter.getNextTimeInMs(); if (nextTime != null) { int nextTimePosition = this.adapter.getPosition(nextTime); if (nextTimePosition > 0) { nextTimePosition--; // show 1 more time on top of the list if (nextTimePosition > 0) { nextTimePosition--; // show 1 more time on top of the list } } if (nextTimePosition >= 0) { return nextTimePosition; } } return 0; } private void initAdapters(Activity activity) { this.adapter = new TimeAdapter(activity, null, null); } private void switchView(View view) { if (view == null) { return; } if (this.adapter == null || !this.adapter.isInitialized()) { showLoading(view); } else if (this.adapter.getCount() == 0) { showEmpty(view); } else { showList(view); } } private void showList(View view) { if (view.findViewById(R.id.loading) != null) { // IF inflated/present DO view.findViewById(R.id.loading).setVisibility(View.GONE); // hide } if (view.findViewById(R.id.empty) != null) { // IF inflated/present DO view.findViewById(R.id.empty).setVisibility(View.GONE); // hide } inflateList(view); view.findViewById(R.id.list).setVisibility(View.VISIBLE); // show } private void inflateList(View view) { if (view.findViewById(R.id.list) == null) { // IF NOT present/inflated DO ((ViewStub) view.findViewById(R.id.list_stub)).inflate(); // inflate } } private void showLoading(View view) { if (view.findViewById(R.id.list) != null) { // IF inflated/present DO view.findViewById(R.id.list).setVisibility(View.GONE); // hide } if (view.findViewById(R.id.empty) != null) { // IF inflated/present DO view.findViewById(R.id.empty).setVisibility(View.GONE); // hide } view.findViewById(R.id.loading).setVisibility(View.VISIBLE); // show } private void showEmpty(View view) { if (view.findViewById(R.id.list) != null) { // IF inflated/present DO view.findViewById(R.id.list).setVisibility(View.GONE); // hide } if (view.findViewById(R.id.loading) != null) { // IF inflated/present DO view.findViewById(R.id.loading).setVisibility(View.GONE); // hide } if (view.findViewById(R.id.empty) == null) { // IF NOT present/inflated DO ((ViewStub) view.findViewById(R.id.empty_stub)).inflate(); // inflate } view.findViewById(R.id.empty).setVisibility(View.VISIBLE); // show } @Override public void onDestroy() { super.onDestroy(); TaskUtils.cancelQuietly(this.loadRtsTask, true); } private static class TimeAdapter extends MTBaseAdapter implements TimeUtils.TimeChangedReceiver.TimeChangedListener { private static final String TAG = ScheduleDayFragment.class.getSimpleName() + ">" + TimeAdapter.class.getSimpleName(); @Override public String getLogTag() { return TAG; } private static final int ITEM_VIEW_TYPE_HOUR_SERATORS = 0; private static final int ITEM_VIEW_TYPE_TIME = 1; private static final int HOUR_SEPARATORS_COUNT = 24; private int timesCount = 0; private SparseArray<ArrayList<Schedule.Timestamp>> hourToTimes = new SparseArray<ArrayList<Schedule.Timestamp>>(); private boolean initialized = false; private ArrayList<Date> hours = new ArrayList<Date>(); private LayoutInflater layoutInflater; private Calendar dayStartsAt; private WeakReference<Activity> activityWR; private Schedule.Timestamp nextTimeInMs = null; private RouteTripStop optRts; private TimeZone deviceTimeZone = TimeZone.getDefault(); public TimeAdapter(Activity activity, Calendar dayStartsAt, RouteTripStop optRts) { super(); setActivity(activity); this.layoutInflater = LayoutInflater.from(activity); setDayStartsAt(dayStartsAt); setRts(optRts); } public void setDayStartsAt(Calendar dayStartsAt) { this.dayStartsAt = dayStartsAt; resetHours(); if (this.dayStartsAt != null) { initHours(); } } public void setRts(RouteTripStop optRts) { this.optRts = optRts; } public void onResume(Activity activity) { setActivity(activity); } public void setActivity(Activity activity) { this.activityWR = new WeakReference<Activity>(activity); } public void onPause() { disableTimeChangeddReceiver(); } private void initHours() { resetHours(); Calendar cal; for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { cal = (Calendar) this.dayStartsAt.clone(); cal.set(Calendar.HOUR_OF_DAY, hourOfTheDay); this.hours.add(cal.getTime()); this.hourToTimes.put(hourOfTheDay, new ArrayList<Schedule.Timestamp>()); } } private void resetHours() { this.hours.clear(); this.hourToTimes.clear(); } public void clearTimes() { for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { this.hourToTimes.get(hourOfTheDay).clear(); } this.timesCount = 0; this.nextTimeInMs = null; } public void setTimes(ArrayList<Schedule.Timestamp> times) { clearTimes(); for (Schedule.Timestamp time : times) { addTime(time); if (this.nextTimeInMs == null && time.t >= getNowToTheMinute()) { this.nextTimeInMs = time; MTLog.d(this, "setTimes() > this.nextTimeInMs: %s", this.nextTimeInMs); } } this.initialized = true; } public boolean isInitialized() { return this.initialized; } @Override public void notifyDataSetChanged() { findNextTimeInMs(); super.notifyDataSetChanged(); } private void findNextTimeInMs() { this.nextTimeInMs = null; for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { for (Schedule.Timestamp time : this.hourToTimes.get(hourOfTheDay)) { if (this.nextTimeInMs == null && time.t >= getNowToTheMinute()) { this.nextTimeInMs = time; return; } } } } public Schedule.Timestamp getNextTimeInMs() { if (this.nextTimeInMs == null) { findNextTimeInMs(); } return nextTimeInMs; } private long nowToTheMinute = -1l; private boolean timeChangedReceiverEnabled = false; private long getNowToTheMinute() { if (this.nowToTheMinute < 0) { resetNowToTheMinute(); enableTimeChangedReceiver(); } return this.nowToTheMinute; } @Override public void onTimeChanged() { resetNowToTheMinute(); } private void resetNowToTheMinute() { this.nowToTheMinute = TimeUtils.currentTimeToTheMinuteMillis(); notifyDataSetChanged(); } private void enableTimeChangedReceiver() { if (!this.timeChangedReceiverEnabled) { Activity activity = this.activityWR == null ? null : this.activityWR.get(); if (activity != null) { activity.registerReceiver(this.timeChangedReceiver, TimeUtils.TIME_CHANGED_INTENT_FILTER); } this.timeChangedReceiverEnabled = true; } } private void disableTimeChangeddReceiver() { if (this.timeChangedReceiverEnabled) { Activity activity = this.activityWR == null ? null : this.activityWR.get(); if (activity != null) { activity.unregisterReceiver(this.timeChangedReceiver); } this.timeChangedReceiverEnabled = false; this.nowToTheMinute = -1l; } } private final BroadcastReceiver timeChangedReceiver = new TimeUtils.TimeChangedReceiver(this); private void addTime(Schedule.Timestamp time) { int hourOfTheDay = TimeUtils.getHourOfTheDay(time.t); this.hourToTimes.get(hourOfTheDay).add(time); this.timesCount++; } @Override public int getCountMT() { return this.timesCount + HOUR_SEPARATORS_COUNT; } @Override public int getViewTypeCount() { return 2; } @Override public Object getItem(int position) { return getItemMT(position); } @Override public Object getItemMT(int position) { int index = 0; for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { index++; // separator if (position >= index && position < index + this.hourToTimes.get(hourOfTheDay).size()) { return this.hourToTimes.get(hourOfTheDay).get(position - index); } index += this.hourToTimes.get(hourOfTheDay).size(); } return null; } private Calendar todayStartsAt = TimeUtils.getBeginningOfTodayCal(); private Calendar todayEndsAt = null; public int compareToToday() { if (this.dayStartsAt.before(this.todayStartsAt)) { return -1; // past } if (this.todayEndsAt == null) { this.todayEndsAt = TimeUtils.getBeginningOfTomorrowCal(); this.todayEndsAt.add(Calendar.MILLISECOND, -1); } if (this.dayStartsAt.after(this.todayEndsAt)) { return +1; // future } return 0; // today } public int getPosition(Object item) { int index = 0; if (item == null || !(item instanceof Schedule.Timestamp)) { return index; } Schedule.Timestamp time = (Schedule.Timestamp) item; Date date = new Date(time.t); Date thatDate; Date nextDate; int nextHourOfTheDay; for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { index++; // separator if (this.hourToTimes.get(hourOfTheDay).size() > 0) { thatDate = this.hours.get(hourOfTheDay); if (date.after(thatDate)) { nextHourOfTheDay = hourOfTheDay + 1; nextDate = nextHourOfTheDay < this.hours.size() ? this.hours.get(nextHourOfTheDay) : null; if (nextDate == null || date.before(nextDate)) { for (Schedule.Timestamp hourTime : this.hourToTimes.get(hourOfTheDay)) { if (time.t == hourTime.t) { return index; } index++; // after } } else { index += this.hourToTimes.get(hourOfTheDay).size(); // after } } else { index += this.hourToTimes.get(hourOfTheDay).size(); // after } } } return -1; } public Integer getItemHourSeparator(int position) { int index = 0; for (int hourOfTheDay = 0; hourOfTheDay < HOUR_SEPARATORS_COUNT; hourOfTheDay++) { if (index == position) { return hourOfTheDay; } index++; index += this.hourToTimes.get(hourOfTheDay).size(); } return null; } @Override public int getItemViewType(int position) { Object item = getItem(position); if (item != null && item instanceof Schedule.Timestamp) { return ITEM_VIEW_TYPE_TIME; } return ITEM_VIEW_TYPE_HOUR_SERATORS; } @Override public long getItemIdMT(int position) { return position; } @Override public View getViewMT(int position, View convertView, ViewGroup parent) { switch (getItemViewType(position)) { case ITEM_VIEW_TYPE_HOUR_SERATORS: return getHourSeparatorView(position, convertView, parent); case ITEM_VIEW_TYPE_TIME: return getTimeView(position, convertView, parent); default: MTLog.w(this, "Unexpected view type at position '%s'!", position); return null; } } private View getTimeView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = this.layoutInflater.inflate(R.layout.layout_poi_detail_status_schedule_time, parent, false); TimeViewHolder holder = new TimeViewHolder(); holder.timeTv = (TextView) convertView.findViewById(R.id.time); convertView.setTag(holder); } updateTimeView(position, convertView); return convertView; } private static final String P2 = ")"; private static final String P1 = " ("; private void updateTimeView(int position, View convertView) { TimeViewHolder holder = (TimeViewHolder) convertView.getTag(); Schedule.Timestamp timestamp = (Schedule.Timestamp) getItem(position); Context context = this.activityWR == null ? null : this.activityWR.get(); if (timestamp != null && context != null) { String userTime = TimeUtils.formatTime(context, timestamp.t); SpannableStringBuilder timeSb = new SpannableStringBuilder(userTime); TimeZone timestampTZ = TimeZone.getTimeZone(timestamp.getLocalTimeZone()); if (timestamp.hasLocalTimeZone() && !this.deviceTimeZone.equals(timestampTZ)) { String localTime = TimeUtils.formatTime(context, timestamp.t, timestampTZ); if (!localTime.equalsIgnoreCase(userTime)) { timeSb.append(P1).append(context.getString(R.string.local_time_and_time, localTime)).append(P2); } } String timeOnly = timeSb.toString(); if (timestamp.hasHeadsign()) { String timestampHeading = timestamp.getHeading(context); String tripHeading = this.optRts == null ? null : this.optRts.getTrip().getHeading(context); if (!Trip.isSameHeadsign(timestampHeading, tripHeading)) { timeSb.append(P1).append(timestampHeading).append(P2); } } TimeUtils.cleanTimes(timeOnly, timeSb); holder.timeTv.setText(timeSb); if (this.nextTimeInMs != null && TimeUtils.isSameDay(getNowToTheMinute(), this.nextTimeInMs.t) && this.nextTimeInMs.t == timestamp.t) { // now holder.timeTv.setTextColor(Schedule.getDefaultNowTextColor(context)); holder.timeTv.setTypeface(Schedule.getDefaultNowTypeface()); } else if (timestamp.t < getNowToTheMinute()) { // past holder.timeTv.setTextColor(Schedule.getDefaultPastTextColor(context)); holder.timeTv.setTypeface(Schedule.getDefaultPastTypeface()); } else { // future holder.timeTv.setTextColor(Schedule.getDefaultFutureTextColor(context)); holder.timeTv.setTypeface(Schedule.getDefaultFutureTypeface()); } } else { holder.timeTv.setText(null); } } private View getHourSeparatorView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = this.layoutInflater.inflate(R.layout.layout_poi_detail_status_schedule_hour_separator, parent, false); HourSperatorViewHolder holder = new HourSperatorViewHolder(); holder.hourTv = (TextView) convertView.findViewById(R.id.hour); convertView.setTag(holder); } updateHourSeparatorView(position, convertView); return convertView; } private void updateHourSeparatorView(int position, View convertView) { HourSperatorViewHolder holder = (HourSperatorViewHolder) convertView.getTag(); Integer hourOfTheDay = getItemHourSeparator(position); Context context = this.activityWR == null ? null : this.activityWR.get(); if (hourOfTheDay != null && context != null) { holder.hourTv.setText(getHourFormatter(context).formatThreadSafe(this.hours.get(hourOfTheDay))); } else { holder.hourTv.setText(null); } } private static ThreadSafeDateFormatter HOUR_FORMATTER; private static ThreadSafeDateFormatter getHourFormatter(Context context) { if (HOUR_FORMATTER == null) { HOUR_FORMATTER = TimeUtils.getNewHourFormat(context); } return HOUR_FORMATTER; } public static class TimeViewHolder { TextView timeTv; } public static class HourSperatorViewHolder { TextView hourTv; } } }