package com.stanleycen.facebookanalytics; import android.app.Fragment; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.AsyncTask; import android.os.Bundle; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import com.haarman.listviewanimations.swinginadapters.prepared.SwingBottomInAnimationAdapter; import com.stanleycen.facebookanalytics.graph.Bar; import com.stanleycen.facebookanalytics.graph.Line; import com.stanleycen.facebookanalytics.graph.LineGraph; import com.stanleycen.facebookanalytics.graph.LinePoint; import com.stanleycen.facebookanalytics.graph.PieSlice; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by scen on 8/30/13. */ public class ConversationFragment extends Fragment { public FBThread fbThread; SpinnerClickReceiver receiver; CardAdapter ca; ListView list; AggregateCounter msgCounter = new AggregateCounter() { @Override public int count(FBMessage message) { return 1; } }; AggregateCounter charCounter = new AggregateCounter() { @Override public int count(FBMessage message) { return message.body.length(); } }; public enum CardItems { TOTAL, PIE_MSG, PIE_CHAR, PIE_SENTFROM, LOADER, BAR_DOW, BAR_CPM, LINE_DAY, LINE_NIGHT, HISTORY_MSG, HISTORY_CHAR } ; public ConversationFragment() { Log.w("Created", "receiver"); receiver = new SpinnerClickReceiver(); } public static Fragment newInstance(Context context, FBThread fbThread) { ConversationFragment f = new ConversationFragment(); f.fbThread = fbThread; return f; } @Override public void onPause() { super.onPause(); Log.w("s", "onPause"); getActivity().unregisterReceiver(receiver); } @Override public void onResume() { super.onResume(); getActivity().getActionBar().setTitle(fbThread.title); getActivity().getActionBar().setSubtitle("" + DateUtils.getRelativeTimeSpanString(fbThread.lastUpdate.getMillis(), DateTime.now().getMillis(), DateUtils.MINUTE_IN_MILLIS, 0)); ((MainActivity) getActivity()).unselectAllFromNav(); IntentFilter filter = new IntentFilter("com.stanleycen.facebookanalytics.spinner.group"); filter.addCategory("android.intent.category.DEFAULT"); getActivity().registerReceiver(receiver, filter); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceBundle) { FBData fbData = GlobalApp.get().fb.fbData; if (fbData == null || fbData.lastUpdate == null) { return (ViewGroup) inflater.inflate(R.layout.fragment_error_data, null); } ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_conversation, null); list = (ListView) root.findViewById(R.id.listView); Util.addSeparatingHeaderView(getActivity(), inflater, list); final List<CardItem> items = new ArrayList<CardItem>(); items.add(new CardLoader(CardItems.LOADER.ordinal(), "Loading conversation")); ca = new CardAdapter(getActivity(), items, CardItems.values().length); SwingBottomInAnimationAdapter swingBottomInAnimationAdapter = new SwingBottomInAnimationAdapter(ca); swingBottomInAnimationAdapter.setAbsListView(list); list.setAdapter(swingBottomInAnimationAdapter); setRetainInstance(true); new Worker().execute(); return root; } private class Worker extends AsyncTask<Void, Void, List<CardItem>> { @Override protected List<CardItem> doInBackground(Void... params) { Intent i = new Intent("com.stanleycen.facebookanalytics.update"); i.putExtra("action", "indeterminate"); getActivity().sendBroadcast(i); List<CardItem> ret = new ArrayList<CardItem>(); Map<FBUser, MutableInt> charCount = new HashMap<FBUser, MutableInt>(); Map<FBUser, MutableInt> msgCount = new HashMap<FBUser, MutableInt>(); fbThread.msgCount = (HashMap) msgCount; Map<FBUser, int[]> userMsgsPerHour = new HashMap<FBUser, int[]>(); int[] messagesPerDow = new int[8]; int[] messagesPerHour = new int[24]; int mobileCount = 0; int webCount = 0; int otherCount = 0; for (FBMessage fbMessage : fbThread.messages) { MutableInt cc = charCount.get(fbMessage.from); if (cc == null) charCount.put(fbMessage.from, new MutableInt(fbMessage.body.length())); else cc.add(fbMessage.body.length()); MutableInt mc = msgCount.get(fbMessage.from); if (mc == null) msgCount.put(fbMessage.from, new MutableInt()); else mc.increment(); messagesPerDow[fbMessage.timestamp.getDayOfWeek()]++; messagesPerHour[fbMessage.timestamp.getHourOfDay()]++; int hour = fbMessage.timestamp.getHourOfDay(); int[] userMPH = userMsgsPerHour.get(fbMessage.from); if (userMPH == null) { userMsgsPerHour.put(fbMessage.from, new int[24]); } userMsgsPerHour.get(fbMessage.from)[hour]++; switch (fbMessage.source) { case MOBILE: mobileCount++; break; case WEB: case OTHER: webCount++; break; default: } } ret.add(new CardTotal(CardItems.TOTAL.ordinal(), fbThread)); CardPieChart msgCard = new CardPieChart(CardItems.PIE_MSG.ordinal(), "Message distribution"); CardPieChart charCard = new CardPieChart(CardItems.PIE_CHAR.ordinal(), "Character distribution"); CardBarChart charsPerMessage = new CardBarChart(CardItems.BAR_CPM.ordinal(), "Characters per message"); ArrayList<Bar> cpmBars = new ArrayList<Bar>(); ArrayList<PieSlice> msgSlices = new ArrayList<PieSlice>(), charSlices = new ArrayList<PieSlice>(); int idx = 0; msgCard.setSlices(msgSlices); charCard.setSlices(charSlices); ret.add(msgCard); ret.add(charCard); for (FBUser person : fbThread.participants) { String name = person == GlobalApp.get().fb.fbData.me ? "You" : person.name; name = name.split(" ")[0]; Bar b = new Bar(); b.setColor(Util.colors[idx % Util.colors.length]); if (msgCount.get(person) != null) b.setValue(msgCount.get(person).get() == 0 ? 0 : (float) charCount.get(person).get() / (float) msgCount.get(person).get()); else b.setValue(0); b.setName(name); cpmBars.add(b); PieSlice slice = new PieSlice(); slice.setColor(Util.colors[idx % Util.colors.length]); slice.setTitle(name); if (msgCount.get(person) != null) slice.setValue(msgCount.get(person).get()); else slice.setValue(0); msgSlices.add(slice); slice = new PieSlice(); slice.setColor(Util.colors[idx % Util.colors.length]); slice.setTitle(name); if (charCount.get(person) != null) slice.setValue(charCount.get(person).get()); else slice.setValue(0); charSlices.add(slice); ++idx; } charsPerMessage.setBars(cpmBars); ret.add(charsPerMessage); CardBarChart mostActiveDow = new CardBarChart(CardItems.BAR_DOW.ordinal(), "Most active day of week"); int firstDow = Util.getJodaFirstDayOfWeek(); ArrayList<Bar> dowBars = new ArrayList<Bar>(); final DateTime tmp = new DateTime(); for (int offset = 0; offset < 7; offset++) { Bar b = new Bar(); b.setName(tmp.withDayOfWeek((firstDow - 1 + offset) % 7 + 1).dayOfWeek().getAsShortText()); b.setColor(Util.colors[offset % Util.colors.length]); b.setValue(messagesPerDow[(firstDow - 1 + offset) % 7 + 1]); dowBars.add(b); } mostActiveDow.setBars(dowBars); ret.add(mostActiveDow); CardLineChart daytimeActivity = new CardLineChart(CardItems.LINE_DAY.ordinal(), "Daytime activity"); CardLineChart nighttimeActivity = new CardLineChart(CardItems.LINE_NIGHT.ordinal(), "Nighttime activity"); final DateTimeFormatter hourFormatter = DateTimeFormat.forPattern("h a"); Map<FBUser, Line> userDaytimeLines = new HashMap<FBUser, Line>(); Map<FBUser, Line> userNighttimeLines = new HashMap<FBUser, Line>(); idx = 0; for (FBUser user : fbThread.participants) { String name = user == GlobalApp.get().fb.fbData.me ? "You" : user.name; name = name.split(" ")[0]; if (msgCount.get(user) != null) { Line line = new Line(); line.setName(name); line.setShowingPoints(true); line.setColor(Util.colors[idx % Util.colors.length]); userDaytimeLines.put(user, line); Line line2 = new Line(); line2.setName(name); line2.setShowingPoints(true); line2.setColor(Util.colors[idx % Util.colors.length]); userNighttimeLines.put(user, line2); idx++; } } Line daytimeLine = new Line(); daytimeLine.setName("Total"); daytimeLine.setShowingPoints(true); daytimeLine.setColor(Util.colors[idx]); Line nighttimeLine = new Line(); nighttimeLine.setName("Total"); nighttimeLine.setShowingPoints(true); nighttimeLine.setColor(Util.colors[idx]); int daytimemx = 0; int nighttimemx = 0; for (int h = 6; h <= 17; h++) { //6am to 5pm daytimeLine.addPoint(new LinePoint(h, messagesPerHour[h])); for (Map.Entry<FBUser, Line> entry : userDaytimeLines.entrySet()) { int[] da = userMsgsPerHour.get(entry.getKey()); entry.getValue().addPoint(new LinePoint(h, da == null ? 0 : da[h])); } daytimemx = Math.max(daytimemx, messagesPerHour[h]); } int iidx = 0; for (int h = 18; h <= 23; h++) { nighttimeLine.addPoint(new LinePoint(iidx, messagesPerHour[h])); for (Map.Entry<FBUser, Line> entry : userNighttimeLines.entrySet()) { int[] da = userMsgsPerHour.get(entry.getKey()); entry.getValue().addPoint(new LinePoint(iidx, da == null ? 0 : da[h])); } iidx++; nighttimemx = Math.max(nighttimemx, messagesPerHour[h]); } for (int h = 0; h <= 5; h++) { nighttimeLine.addPoint(new LinePoint(iidx, messagesPerHour[h])); for (Map.Entry<FBUser, Line> entry : userNighttimeLines.entrySet()) { int[] da = userMsgsPerHour.get(entry.getKey()); entry.getValue().addPoint(new LinePoint(iidx, da == null ? 0 : da[h])); } iidx++; nighttimemx = Math.max(nighttimemx, messagesPerHour[h]); } daytimeActivity.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return hourFormatter.print(tmp.withHourOfDay((idx * ptsPerDelta) + 6)); } }); daytimeActivity.setyFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return (int) ((max - min) * ((float) idx / (float) (tot - 1)) + min) + (idx == tot - 1 ? " messages" : ""); } }); nighttimeActivity.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return hourFormatter.print(tmp.withHourOfDay((idx * ptsPerDelta) + 6).plusHours(12)); } }); nighttimeActivity.setyFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return (int) ((max - min) * ((float) idx / (float) (tot - 1)) + min) + (idx == tot - 1 ? " messages" : ""); } }); ArrayList<Line> daytimeLines = new ArrayList<Line>(), nighttimeLines = new ArrayList<Line>(); for (Map.Entry<FBUser, Line> entry : userDaytimeLines.entrySet()) { daytimeLines.add(entry.getValue()); } for (Map.Entry<FBUser, Line> entry : userNighttimeLines.entrySet()) { nighttimeLines.add(entry.getValue()); } daytimeLines.add(daytimeLine); nighttimeLines.add(nighttimeLine); daytimeActivity.setLines(daytimeLines); nighttimeActivity.setLines(nighttimeLines); daytimeActivity.setRangeY(0, Util.roundUpNiceDiv4(daytimemx)); nighttimeActivity.setRangeY(0, Util.roundUpNiceDiv4(nighttimemx)); ret.add(daytimeActivity); ret.add(nighttimeActivity); CardLineChartSpinner msghistory = new CardLineChartSpinner(CardItems.HISTORY_MSG.ordinal(), "Message history over time"); loadHistory(msghistory, 1, msgCounter, " messages"); ret.add(msghistory); CardLineChartSpinner charhistory = new CardLineChartSpinner(CardItems.HISTORY_CHAR.ordinal(), "Character history over time"); loadHistory(charhistory, 1, charCounter, " characters"); ret.add(charhistory); CardPieChart sentFromCard = new CardPieChart(CardItems.PIE_SENTFROM.ordinal(), "Devices sent from"); PieSlice webSlice = new PieSlice(); webSlice.setColor(Util.colors[0]); webSlice.setTitle("Web"); webSlice.setValue(webCount); PieSlice mobileSlice = new PieSlice(); mobileSlice.setColor(Util.colors[1]); mobileSlice.setTitle("Mobile"); mobileSlice.setValue(mobileCount); // PieSlice otherSlice = new PieSlice(); // otherSlice.setColor(Util.colors[2]); // otherSlice.setTitle("Other"); // otherSlice.setValue(otherCount); sentFromCard.setSlices(new ArrayList<PieSlice>(Arrays.asList(webSlice, mobileSlice))); ret.add(sentFromCard); return ret; } @Override protected void onPostExecute(List<CardItem> result) { ca.clear(); ca.addAll(result); } } DateTime getBucketEndpoint(DateTime start, int bucketSize) { switch (bucketSize) { case 0: return start.plusDays(1); case 1: return start.plusWeeks(1); case 2: return start.plusMonths(1); case 3: return start.plusYears(1); default: Log.w("loadHistory", "unknown bucketSize"); } return start; } void loadHistory(CardLineChartSpinner card, int bucketSize, AggregateCounter counter, final String suffix) { /* 0=day 1=week 2=month 3=year */ if (fbThread.messages.isEmpty()) return; DateTime startDate = fbThread.messages.get(0).timestamp.withTimeAtStartOfDay(); DateTime bucketEndpoint = getBucketEndpoint(startDate, bucketSize); Interval curBucket = new Interval(startDate, bucketEndpoint); DateTime endDate = DateTime.now().withTimeAtStartOfDay(); Line totalLine = new Line(); Map<FBUser, Line> userLines = new HashMap<FBUser, Line>(); for (FBUser user : fbThread.participants) { if (fbThread.msgCount.get(user) != null) userLines.put(user, new Line()); } int msgIndex = 0; int size = fbThread.messages.size(); Map<FBUser, MutableInt> curBucketUserCount = new HashMap<FBUser, MutableInt>(); int idx = 0; int accum = 0; int maxval = 0; while (true) { if (msgIndex >= size) break; int curBucketTotal = 0; curBucketUserCount.clear(); for (; msgIndex < size; ) { FBMessage msg = fbThread.messages.get(msgIndex); DateTime normalized = msg.timestamp.withTimeAtStartOfDay(); if (curBucket.contains(normalized)) { int cnt = counter.count(msg); curBucketTotal += cnt; MutableInt i = curBucketUserCount.get(msg.from); if (i == null) { curBucketUserCount.put(msg.from, new MutableInt(cnt)); } else { i.add(cnt); } msgIndex++; } else { break; } } totalLine.addPoint(new LinePoint(idx, curBucketTotal)); for (Map.Entry<FBUser, Line> entry : userLines.entrySet()) { MutableInt val = curBucketUserCount.get(entry.getKey()); entry.getValue().addPoint(new LinePoint(idx, val == null ? 0 : val.get())); } accum += curBucketTotal; maxval = Math.max(maxval, curBucketTotal); // Log.v("dbg", "idx = " + idx + " tot = " + curBucketTotal + " accum = " + accum); idx++; startDate = bucketEndpoint; bucketEndpoint = getBucketEndpoint(startDate, bucketSize); curBucket = new Interval(startDate, bucketEndpoint); if (startDate.isAfter(endDate)) break; } boolean showPoints = totalLine.getPoints().size() <= 30; idx = 0; ArrayList<Line> lines = new ArrayList<Line>(); for (Map.Entry<FBUser, Line> entry : userLines.entrySet()) { Line l = entry.getValue(); String name = entry.getKey() == GlobalApp.get().fb.fbData.me ? "You" : entry.getKey().name; name = name.split(" ")[0]; l.setColor(Util.colors[idx % Util.colors.length]); l.setName(name); l.setShowingPoints(showPoints); lines.add(l); idx++; } totalLine.setColor(Util.colors[idx % Util.colors.length]); totalLine.setName("Total"); totalLine.setShowingPoints(showPoints); lines.add(totalLine); card.setLines(lines); card.setShouldCacheToBitmap(true); card.setRangeY(0, Util.roundUpNiceDiv4((float) maxval)); card.setyFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return (int) ((max - min) * ((float) idx / (float) (tot - 1)) + min) + (idx == tot - 1 ? suffix : ""); } }); final DateTime s = fbThread.messages.get(0).timestamp.withTimeAtStartOfDay(); switch (bucketSize) { case 0: card.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { if (idx == 0) return initialDayFormatter.print(s.plusDays(idx * ptsPerDelta)); else return dayFormatter.print(s.plusDays(idx * ptsPerDelta)); } }); break; case 1: card.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { if (idx == 0) return initialDayFormatter.print(s.plusWeeks(idx * ptsPerDelta)); else return dayFormatter.print(s.plusWeeks(idx * ptsPerDelta)); } }); break; case 2: card.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { if (idx == 0) return initialMonthFormatter.print(s.plusMonths(idx * ptsPerDelta)); else return monthFormatter.print(s.plusMonths(idx * ptsPerDelta)); } }); break; case 3: card.setxFormatter(new LineGraph.LabelFormatter() { @Override public String format(int idx, int tot, float min, float max, int ptsPerDelta) { return yearFormatter.print(s.plusYears(idx * ptsPerDelta)); } }); break; } } DateTimeFormatter initialDayFormatter = DateTimeFormat.forPattern("MMM d ''yy"); DateTimeFormatter dayFormatter = DateTimeFormat.forPattern("MMM d"); DateTimeFormatter initialMonthFormatter = DateTimeFormat.forPattern("MMM ''yy"); DateTimeFormatter monthFormatter = DateTimeFormat.forPattern("MMM"); DateTimeFormatter yearFormatter = DateTimeFormat.forPattern("yyyy"); class SpinnerClickReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String title = intent.getStringExtra("title"); int value = intent.getIntExtra("value", 0); // ca.remove(5); if (title.equals("Message history over time")) { int size = ca.getCount(); int idx = 0; for (idx = 0; idx < size; idx++) { if (ca.getItem(idx) instanceof CardLineChartSpinner && ((CardLineChartSpinner) ca.getItem(idx)).title.equals("Message history over time")) { CardLineChartSpinner card = ((CardLineChartSpinner) ca.getItem(idx)); loadHistory(card, value, msgCounter, " messages"); card.refreshLineChart(); card.invalidateChart(); break; } } } if (title.equals("Character history over time")) { int size = ca.getCount(); int idx = 0; for (idx = 0; idx < size; idx++) { if (ca.getItem(idx) instanceof CardLineChartSpinner && ((CardLineChartSpinner) ca.getItem(idx)).title.equals("Character history over time")) { CardLineChartSpinner card = ((CardLineChartSpinner) ca.getItem(idx)); loadHistory(card, value, charCounter, " characters"); card.refreshLineChart(); card.invalidateChart(); break; } } } } } private abstract interface AggregateCounter { public abstract int count(FBMessage message); } }