package com.sunlightlabs.android.congress;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.commonsware.cwac.merge.MergeAdapter;
import com.sunlightlabs.android.congress.tasks.LoadPhotoTask;
import com.sunlightlabs.android.congress.utils.ActionBarUtils;
import com.sunlightlabs.android.congress.utils.Analytics;
import com.sunlightlabs.android.congress.utils.Database;
import com.sunlightlabs.android.congress.utils.LegislatorImage;
import com.sunlightlabs.android.congress.utils.Utils;
import com.sunlightlabs.congress.models.Amendment;
import com.sunlightlabs.congress.models.Bill;
import com.sunlightlabs.congress.models.CongressException;
import com.sunlightlabs.congress.models.Legislator;
import com.sunlightlabs.congress.models.Nomination;
import com.sunlightlabs.congress.models.Roll;
import com.sunlightlabs.congress.models.Roll.Vote;
import com.sunlightlabs.congress.services.RollService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
/*
* Eric's notes - this class should be refactored into a RollPager activity,
* and a RollInfoFragment. This would remove a lot of the screen-flipping logic
* from this class, and bring it into line with the best practices elsewhere.
* Cribbing from LegislatorProfileFragment and BillInfoFragment would make this easy.
*
* This would also make it easier to make new tabs, such as showing a breakdown
* of the votes by party, etc. *
*/
public class RollInfo extends ListActivity implements LoadPhotoTask.LoadsPhoto {
private String id;
private Roll roll;
private Map<String,Roll.Vote> voters;
private Database database;
private Cursor peopleCursor;
private LoadRollTask loadRollTask, loadVotersTask;
private View header, loadingView;
private Map<String,LoadPhotoTask> loadPhotoTasks = new HashMap<String,LoadPhotoTask>();
private List<String> queuedPhotos = new ArrayList<String>();
private static final int MAX_PHOTO_TASKS = 10;
private static final int MAX_QUEUE_TASKS = 20;
// keep the adapters and arrays as members so we can toggle freely between them
private List<Roll.Vote> starred = new ArrayList<Roll.Vote>();
private List<Roll.Vote> rest = new ArrayList<Roll.Vote>();
private VoterAdapter starredAdapter;
private VoterAdapter restAdapter;
private String currentTab = null;
private Map<String,List<Roll.Vote>> voterBreakdown = new HashMap<String,List<Roll.Vote>>();
LayoutInflater inflater;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Analytics.init(this);
setContentView(R.layout.list_titled_fastscroll);
inflater = LayoutInflater.from(this);
database = new Database(this);
database.open();
peopleCursor = database.getLegislators();
startManagingCursor(peopleCursor);
Bundle extras = getIntent().getExtras();
id = extras.getString("id");
roll = (Roll) extras.getSerializable("roll");
Intent intent = getIntent();
if (intent != null) {
Uri uri = intent.getData();
if (uri != null) {
List<String> segments = uri.getPathSegments();
if (segments.size() == 4) { // coming in from floor
String chamber = segments.get(1);
String year = segments.get(2);
String formattedNumber = segments.get(3);
id = Roll.normalizeRollId(chamber, year, formattedNumber);
}
}
}
setupControls();
RollInfoHolder holder = (RollInfoHolder) getLastNonConfigurationInstance();
if (holder != null) {
this.loadRollTask = holder.loadRollTask;
this.roll = holder.roll;
this.loadVotersTask = holder.loadVotersTask;
this.voters = holder.voters;
this.loadPhotoTasks = holder.loadPhotoTasks;
this.currentTab = holder.currentTab;
if (loadPhotoTasks != null) {
Iterator<LoadPhotoTask> iterator = loadPhotoTasks.values().iterator();
while (iterator.hasNext())
iterator.next().onScreenLoad(this);
}
}
loadRoll();
}
@Override
public Object onRetainNonConfigurationInstance() {
return new RollInfoHolder(loadRollTask, roll, loadVotersTask, voters, loadPhotoTasks, currentTab);
}
@Override
public void onStart() {
super.onStart();
Analytics.start(this);
}
@Override
public void onStop() {
super.onStop();
Analytics.stop(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
database.close();
}
public void setupControls() {
ActionBarUtils.setTitle(this, Utils.formatRollId(id), new Intent(this, MenuVotes.class));
Utils.setLoading(this, R.string.vote_loading);
}
@Override
public void onListItemClick(ListView parent, View view, int position, long id) {
Object tag = view.getTag();
if (tag != null) {
if (tag instanceof VoterAdapter.ViewHolder)
startActivity(Utils.legislatorIntent(((VoterAdapter.ViewHolder) tag).bioguide_id));
}
}
public void loadRoll() {
if (loadRollTask != null)
loadRollTask.onScreenLoad(this);
else {
if (roll != null)
displayRoll();
else
loadRollTask = (LoadRollTask) new LoadRollTask(this, id, "basic").execute(RollService.basicFields);
}
}
public void loadVotes() {
if (loadVotersTask != null)
loadVotersTask.onScreenLoad(this);
else {
if (voters != null)
displayVoters();
else
loadVotersTask = (LoadRollTask) new LoadRollTask(this, id, "voters").execute("voters");
}
}
public void onLoadRoll(String tag, Roll roll) {
if (tag.equals("basic")) {
this.loadRollTask = null;
this.roll = roll;
displayRoll();
} else if (tag.equals("voters")) {
this.loadVotersTask = null;
this.voters = roll.voters;
displayVoters();
}
}
public void onLoadRoll(String tag, CongressException exception) {
if (tag.equals("basic")) {
if (exception instanceof CongressException.NotFound)
Utils.alert(this, R.string.vote_not_found);
else
Utils.alert(this, R.string.error_connection);
this.loadRollTask = null;
finish();
} else if (tag.equals("voters")) {
this.loadVotersTask = null;
View loadingView = findViewById(R.id.loading_votes);
loadingView.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
((TextView) loadingView.findViewById(R.id.loading_message)).setText(R.string.votes_error);
}
}
public void displayRoll() {
LayoutInflater inflater = LayoutInflater.from(this);
MergeAdapter adapter = new MergeAdapter();
View headerTop = inflater.inflate(R.layout.roll_basic_1, null);
((TextView) headerTop.findViewById(R.id.question)).setText(simpleQuestion(roll));
((TextView) headerTop.findViewById(R.id.voted_at)).setText(new SimpleDateFormat("MMM dd, yyyy").format(roll.voted_at).toUpperCase());
if (roll.nomination != null && roll.nomination.nominees != null) {
headerTop.findViewById(R.id.details).setVisibility(View.VISIBLE);
((TextView) headerTop.findViewById(R.id.details_text)).setText(Nomination.nomineesFor(roll.nomination));
} else if (roll.amendment != null && roll.amendment.amends_bill_id != null) {
headerTop.findViewById(R.id.details).setVisibility(View.VISIBLE);
String text = "Amendment to " + Bill.formatCode(roll.amendment.amends_bill_id) + ": " + Amendment.description(roll.amendment);
((TextView) headerTop.findViewById(R.id.details_text)).setText(text);
}
adapter.addView(headerTop);
if (roll.bill_id != null && !roll.bill_id.equals("")) {
View header = inflater.inflate(R.layout.header, null);
TextView related = (TextView) header.findViewById(R.id.header_text);
if (roll.vote_type != null) {
if (roll.vote_type.equals("passage"))
related.setText(R.string.vote_related_to_bill_passage);
else if (roll.vote_type.equals("cloture"))
related.setText(R.string.vote_related_to_bill_cloture);
else
related.setText(R.string.vote_related_to_bill);
} else
related.setText(R.string.vote_related_to_bill);
adapter.addView(header);
View bill = inflater.inflate(R.layout.roll_bill, null);
((TextView) bill.findViewById(R.id.code)).setText(Bill.formatCode(roll.bill_id));
TextView titleView = (TextView) bill.findViewById(R.id.bill_title);
if (roll.bill != null) {
if (roll.bill.short_title != null) {
titleView.setTextSize(16);
titleView.setText(Utils.truncate(roll.bill.short_title, 200));
} else if (roll.bill.official_title != null) {
titleView.setTextSize(14);
titleView.setText(Utils.truncate(roll.bill.official_title, 200));
} else {
titleView.setTextSize(16);
titleView.setText(R.string.bill_no_title);
}
} else {
titleView.setTextSize(16);
titleView.setText(R.string.bill_no_title_yet);
}
bill.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(Utils.billIntent(roll.bill_id));
}
});
adapter.addView(bill);
}
header = inflater.inflate(R.layout.roll_basic_2, null);
View resultHeader = header.findViewById(R.id.result_header);
((TextView) resultHeader.findViewById(R.id.header_text)).setText(R.string.vote_results_header);
String requiredText = roll.required.equals("QUORUM") ? "Quorum" : roll.required + " majority required";
((TextView) resultHeader.findViewById(R.id.header_text_right)).setText(requiredText);
((TextView) header.findViewById(R.id.result)).setText(roll.result);
loadingView = header.findViewById(R.id.loading_votes);
((TextView) loadingView.findViewById(R.id.loading_message)).setText("Loading votes...");
setupTabs();
adapter.addView(header);
setListAdapter(adapter);
// kick off vote loading
loadVotes();
}
// if the roll's about a bill, strip out the bill information from the question
public String simpleQuestion(Roll roll) {
if (roll.bill != null)
return TextUtils.split(roll.question, " -- ")[0];
else
return roll.question;
}
// depends on the "header" member variable having been initialized and inflated
public void setupTabs() {
View.OnClickListener tabListener = new View.OnClickListener() {
public void onClick(View view) {
String tag = (String) view.getTag();
Iterator<String> iter = voterBreakdown.keySet().iterator();
while (iter.hasNext()) {
String tabTag = iter.next();
if (tabTag.equals(tag))
header.findViewWithTag(tabTag).setSelected(true);
else
header.findViewWithTag(tabTag).setSelected(false);
}
currentTab = tag;
toggleVoters(tag);
}
};
LinearLayout tabContainer = (LinearLayout) header.findViewById(R.id.vote_tabs);
// yea and nay should always be first and second, if present
// present and not voting should always be second to last and last
Comparator<String> tabSorter = new Comparator<String>() {
public int compare(String one, String other) {
if (one.equals(Roll.NOT_VOTING))
return 1;
else if (one.equals(Roll.PRESENT)) {
if (other.equals(Roll.NOT_VOTING))
return -1;
else
return 1;
} else if (one.equals(Roll.YEA))
return -1;
else if (one.equals(Roll.NAY)) {
if (other.equals(Roll.YEA))
return 1;
else
return -1;
} else {
if (other.equals(Roll.NOT_VOTING) || other.equals(Roll.PRESENT))
return -1;
else
return one.compareTo(other);
}
}
};
Iterator<String> iter = roll.voteBreakdown.keySet().iterator();
List<String> names = new ArrayList<String>();
while (iter.hasNext())
names.add(iter.next());
Collections.sort(names, tabSorter);
for (int i=0; i<names.size(); i++) {
String name = names.get(i);
if (i == 0 && currentTab == null)
currentTab = name;
addTab(name, tabContainer, tabListener);
}
}
public void addTab(String name, LinearLayout parent, View.OnClickListener tabListener) {
View tab = inflater.inflate(R.layout.tab_2, null);
String displayName;
if (name.equals(Roll.NOT_VOTING))
displayName = getResources().getString(R.string.not_voting_short);
else
displayName = name;
((TextView) tab.findViewById(R.id.name)).setText(displayName);
((TextView) tab.findViewById(R.id.subname)).setText(roll.voteBreakdown.get(name) + "");
tab.setTag(name);
tab.setOnClickListener(tabListener);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
parent.addView(tab, params);
voterBreakdown.put(name, new ArrayList<Roll.Vote>());
}
// depends on setupTabs having been called, and that every vote a legislator has cast
// has an entry in voterBreakdown, as created in setupTabs
public void displayVoters() {
if (voters != null) {
// sort Map of voters into the voterBreakdown Map by vote type
List<Roll.Vote> allVoters = new ArrayList<Roll.Vote>(voters.values());
Collections.sort(allVoters); // sort once, all at once
Iterator<Roll.Vote> iter = allVoters.iterator();
while (iter.hasNext()) {
Roll.Vote vote = iter.next();
voterBreakdown.get(vote.vote).add(vote);
}
// hide loading, show tabs
loadingView.setVisibility(View.GONE);
header.findViewWithTag(currentTab).setSelected(true);
header.findViewById(R.id.vote_tabs).setVisibility(View.VISIBLE);
// initialize adapters, add them beneath the tabs
starredAdapter = new VoterAdapter(this, starred);
restAdapter = new VoterAdapter(this, rest);
MergeAdapter adapter = (MergeAdapter) getListAdapter();
adapter.addAdapter(starredAdapter);
adapter.addAdapter(restAdapter);
setListAdapter(adapter);
// show the voters for the current tab
toggleVoters(currentTab);
} else {
loadingView.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
((TextView) loadingView.findViewById(R.id.loading_message)).setText(R.string.vote_no_voters_yet);
}
}
public void toggleVoters(String tag) {
rest.clear();
starred.clear();
rest.addAll(voterBreakdown.get(tag));
// reset starred, sweep through the new array again
int starredCount = peopleCursor.getCount();
if (starredCount > 0) {
List<String> starredIds = new ArrayList<String>(starredCount);
peopleCursor.moveToFirst();
do {
starredIds.add(peopleCursor.getString(peopleCursor.getColumnIndex("bioguide_id")));
} while(peopleCursor.moveToNext());
Iterator<Roll.Vote> iter = rest.iterator();
while (iter.hasNext()) {
Roll.Vote vote = iter.next();
if (starredIds.contains(vote.voter_id)) {
iter.remove();
starred.add(vote);
}
}
}
((MergeAdapter) getListAdapter()).notifyDataSetChanged();
}
public void loadPhoto(String bioguide_id) {
if (!loadPhotoTasks.containsKey(bioguide_id)) {
// if we have free space, fetch the photo
if (loadPhotoTasks.size() <= MAX_PHOTO_TASKS) {
try {
loadPhotoTasks.put(bioguide_id, (LoadPhotoTask) new LoadPhotoTask(this, LegislatorImage.PIC_SMALL, bioguide_id).execute(bioguide_id));
} catch(RejectedExecutionException e) {
Log.e(Utils.TAG, "[RollInfo] RejectedExecutionException occurred while loading photo.", e);
loadNoPhoto(bioguide_id);
}
}
// otherwise, add it to the queue for later
else {
if (queuedPhotos.size() > MAX_QUEUE_TASKS)
queuedPhotos.clear();
if (!queuedPhotos.contains(bioguide_id))
queuedPhotos.add(bioguide_id);
}
}
}
public void onLoadPhoto(Drawable photo, Object tag) {
loadPhotoTasks.remove(tag);
VoterAdapter.ViewHolder holder = new VoterAdapter.ViewHolder();
holder.bioguide_id = (String) tag;
View result = getListView().findViewWithTag(holder);
if (result != null) {
if (photo != null)
((ImageView) result.findViewById(R.id.photo)).setImageDrawable(photo);
else
((ImageView) result.findViewById(R.id.photo)).setImageResource(R.drawable.person);
}
// if there's any in the queue, send the next one
if (!queuedPhotos.isEmpty())
loadPhoto(queuedPhotos.remove(0));
}
public void loadNoPhoto(String bioguide_id) {
VoterAdapter.ViewHolder holder = new VoterAdapter.ViewHolder();
holder.bioguide_id = bioguide_id;
View result = getListView().findViewWithTag(holder);
if (result != null)
((ImageView) result.findViewById(R.id.photo)).setImageResource(R.drawable.person);
}
public Context getContext() {
return this;
}
private class LoadRollTask extends AsyncTask<String,Void,Roll> {
private RollInfo context;
private CongressException exception;
private String rollId, tag;
public LoadRollTask(RollInfo context, String rollId, String tag) {
this.context = context;
this.rollId = rollId;
this.tag = tag;
Utils.setupAPI(context);
}
public void onScreenLoad(RollInfo context) {
this.context = context;
}
@Override
public Roll doInBackground(String... fields) {
try {
return RollService.find(rollId, fields);
} catch (CongressException exception) {
this.exception = exception;
return null;
}
}
@Override
public void onPostExecute(Roll roll) {
if (isCancelled()) return;
// last check - if the database is closed, then onDestroy must have run,
// even if the task didn't get marked as cancelled for some reason
if (context.database.closed) return;
if (exception != null && roll == null)
context.onLoadRoll(tag, exception);
else
context.onLoadRoll(tag, roll);
}
}
private static class VoterAdapter extends ArrayAdapter<Roll.Vote> {
LayoutInflater inflater;
RollInfo context;
public VoterAdapter(RollInfo context, List<Vote> rest) {
super(context, 0, rest);
this.context = context;
this.inflater = LayoutInflater.from(context);
}
public boolean areAllItemsEnabled() {
return true;
}
@Override
public int getViewTypeCount() {
return 1;
}
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder holder;
if (convertView == null) {
view = inflater.inflate(R.layout.legislator_voter, null);
holder = new ViewHolder();
holder.name = (TextView) view.findViewById(R.id.name);
holder.vote = (TextView) view.findViewById(R.id.vote);
holder.photo = (ImageView) view.findViewById(R.id.photo);
view.setTag(holder);
} else {
view = convertView;
holder = (ViewHolder) view.getTag();
}
Roll.Vote vote = getItem(position);
Legislator legislator = vote.voter;
// used as the hook to get the legislator image in place when it's loaded
// and to link to the legislator's activity
holder.bioguide_id = vote.voter_id;
holder.name.setText(nameFor(legislator));
TextView voteView = holder.vote;
String value = vote.vote;
if (value.equals(Roll.YEA))
voteView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
else if (value.equals(Roll.NAY))
voteView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
else if (value.equals(Roll.PRESENT))
voteView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
else if (value.equals(Roll.NOT_VOTING))
voteView.setTypeface(Typeface.defaultFromStyle(Typeface.NORMAL));
else
voteView.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
voteView.setText(vote.vote);
ImageView photo = (ImageView) view.findViewById(R.id.photo);
LegislatorImage.setImageView(legislator.bioguide_id, LegislatorImage.PIC_SMALL,
context.getContext(), holder.photo);
return view;
}
public String nameFor(Legislator legislator) {
String position = legislator.party + "-" + legislator.state;
return legislator.last_name + ", " + legislator.firstName() + " [" + position + "]";
}
static class ViewHolder {
TextView name, vote;
ImageView photo;
String bioguide_id;
@Override
public boolean equals(Object other) {
return other != null && other instanceof ViewHolder && this.bioguide_id.equals(((ViewHolder) other).bioguide_id);
}
}
}
static class RollInfoHolder {
LoadRollTask loadRollTask, loadVotersTask;
Roll roll;
Map<String,Roll.Vote> voters;
Map<String,LoadPhotoTask> loadPhotoTasks;
String currentTab;
public RollInfoHolder(LoadRollTask loadRollTask, Roll roll, LoadRollTask loadVotersTask, Map<String,Roll.Vote> voters, Map<String,LoadPhotoTask> loadPhotoTasks, String currentTab) {
this.loadRollTask = loadRollTask;
this.roll = roll;
this.loadVotersTask = loadVotersTask;
this.voters = voters;
this.loadPhotoTasks = loadPhotoTasks;
this.currentTab = currentTab;
}
}
}