From 775df53ea8018a147944bb92bf31bd0f4aa351ce Mon Sep 17 00:00:00 2001 From: Katie Dektar Date: Thu, 10 Sep 2026 20:07:57 -0700 Subject: [PATCH 1/2] Daily counts in about activity --- .../a5calls/android/a5calls/FakeJSONData.java | 1 + .../a5calls/controller/AboutActivityTest.java | 3 +- .../android/a5calls/net/FiveCallsApiTest.java | 36 +++++++++++++- .../a5calls/controller/AboutActivity.java | 34 ++++++++++++- .../a5calls/controller/IssueActivity.java | 4 +- .../a5calls/controller/TutorialActivity.java | 5 +- .../a5calls/model/HourlyCallCount.java | 43 +++++++++++++++++ .../android/a5calls/net/FiveCallsApi.java | 48 ++++++++++--------- .../src/main/res/layout/activity_about.xml | 12 ++++- 5calls/app/src/main/res/values-es/strings.xml | 6 +++ 5calls/app/src/main/res/values/strings.xml | 8 +++- 11 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 5calls/app/src/main/java/org/a5calls/android/a5calls/model/HourlyCallCount.java diff --git a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/FakeJSONData.java b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/FakeJSONData.java index d388ea13..893561d4 100644 --- a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/FakeJSONData.java +++ b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/FakeJSONData.java @@ -15,6 +15,7 @@ public class FakeJSONData { // A snapshot of real report data, used for testing. public static final String REPORT_DATA = "{\"count\":4627301,\"donateOn\":true}"; + public static final String REPORT_DATA_FULL = "{\"count\":14074073,\"donateOn\":false,\"serverTime\":1789094984,\"hourlyCalls\":[{\"time\":1789005600,\"count\":106},{\"time\":1789009200,\"count\":109},{\"time\":1789012800,\"count\":125},{\"time\":1789016400,\"count\":63},{\"time\":1789020000,\"count\":25},{\"time\":1789023600,\"count\":5},{\"time\":1789027200,\"count\":61},{\"time\":1789030800,\"count\":50},{\"time\":1789034400,\"count\":97},{\"time\":1789038000,\"count\":291},{\"time\":1789041600,\"count\":290},{\"time\":1789045200,\"count\":211},{\"time\":1789048800,\"count\":211},{\"time\":1789052400,\"count\":315},{\"time\":1789056000,\"count\":163},{\"time\":1789059600,\"count\":175},{\"time\":1789063200,\"count\":241},{\"time\":1789066800,\"count\":150},{\"time\":1789070400,\"count\":251},{\"time\":1789074000,\"count\":270},{\"time\":1789077600,\"count\":293},{\"time\":1789081200,\"count\":211},{\"time\":1789084800,\"count\":221},{\"time\":1789088400,\"count\":136},{\"time\":1789092000,\"count\":62}]}"; /** * Returns the full issue data as a JSONArray. diff --git a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/AboutActivityTest.java b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/AboutActivityTest.java index 959962db..c9279641 100644 --- a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/AboutActivityTest.java +++ b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/AboutActivityTest.java @@ -8,6 +8,7 @@ import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.a5calls.android.a5calls.FakeJSONData.REPORT_DATA; import static org.hamcrest.Matchers.allOf; import android.app.Activity; @@ -57,7 +58,7 @@ public void tearDown() { @Test public void testCheckRegistrationButton_launchesIntent() { // Mock the report response to avoid errors in AboutActivity - mHttpStack.setResponseToReturn(new HttpResponse(200, new ArrayList<>(), "{\"count\": 100}".getBytes())); + mHttpStack.setResponseToReturn(new HttpResponse(200, new ArrayList<>(), REPORT_DATA.getBytes())); aboutScenario = ActivityScenario.launch(AboutActivity.class); diff --git a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/net/FiveCallsApiTest.java b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/net/FiveCallsApiTest.java index 8d1ec9bd..b234f76c 100644 --- a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/net/FiveCallsApiTest.java +++ b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/net/FiveCallsApiTest.java @@ -7,6 +7,7 @@ import com.android.volley.toolbox.HttpResponse; import org.a5calls.android.a5calls.model.Contact; +import org.a5calls.android.a5calls.model.HourlyCallCount; import org.a5calls.android.a5calls.model.Issue; import org.a5calls.android.a5calls.model.Outcome; import org.json.JSONException; @@ -25,6 +26,7 @@ import static org.a5calls.android.a5calls.FakeJSONData.ISSUE_DATA; import static org.a5calls.android.a5calls.FakeJSONData.REPORT_DATA; +import static org.a5calls.android.a5calls.FakeJSONData.REPORT_DATA_FULL; import static org.a5calls.android.a5calls.FakeJSONData.REPS_DATA_SUFFIX; import static org.a5calls.android.a5calls.FakeJSONData.REPS_DATA_NOT_SPLIT_PREFIX; import static org.a5calls.android.a5calls.FakeJSONData.REPS_DATA_SPLIT_PREFIX; @@ -39,6 +41,8 @@ static class TestCallListener implements FiveCallsApi.CallRequestListener { protected int mCallReported = 0; protected int mCallCount = 0; protected boolean mDonateOn = false; + protected long mServerTime = 0; + protected List mHourlyCounts = null; @Override public void onRequestError() { @@ -51,9 +55,12 @@ public void onJsonError() { } @Override - public void onReportReceived(int count, boolean donateOn) { + public void onReportReceived(int count, boolean donateOn, long serverTime, + List hourlyCounts) { mCallCount = count; mDonateOn = donateOn; + mServerTime = serverTime; + mHourlyCounts = hourlyCounts; } @Override @@ -153,7 +160,7 @@ public void tearDown() { } @Test - public void testGetCallCount() { + public void testGetCallCount_noHourlyCounts() { byte[] bytes = REPORT_DATA.getBytes(); ArrayList
headers = new ArrayList<>(); headers.add(new Header("Content-Type", "text/json")); @@ -174,6 +181,31 @@ public void testGetCallCount() { mApi.unregisterCallRequestListener(testCallListener); } + @Test + public void testGetCallCount_hourlyCounts() { + byte[] bytes = REPORT_DATA_FULL.getBytes(); + ArrayList
headers = new ArrayList<>(); + headers.add(new Header("Content-Type", "text/json")); + HttpResponse response = new HttpResponse(200, headers, bytes); + mHttpStack.setResponseToReturn(response); + + TestCallListener testCallListener = new TestCallListener(); + mApi.registerCallRequestListener(testCallListener); + mApi.getReport(); + waitForHttpRequestComplete(); + + assertEquals(0, testCallListener.mCallError); + assertEquals(0, testCallListener.mCallJsonError); + assertEquals(0, testCallListener.mCallReported); + assertEquals(14074073, testCallListener.mCallCount); + assertFalse(testCallListener.mDonateOn); + + assertEquals(1789094984, testCallListener.mServerTime); + assertEquals(25, testCallListener.mHourlyCounts.size()); + + mApi.unregisterCallRequestListener(testCallListener); + } + @Test public void testGetCallCount_serverError() { mHttpStack.setExceptionToThrow(new IOException("HTTP Stack exception")); diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/AboutActivity.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/AboutActivity.java index 67d6225b..7f3fb0da 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/AboutActivity.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/AboutActivity.java @@ -31,11 +31,15 @@ import org.a5calls.android.a5calls.FiveCallsApplication; import org.a5calls.android.a5calls.databinding.ActivityAboutBinding; import org.a5calls.android.a5calls.model.AccountManager; +import org.a5calls.android.a5calls.model.HourlyCallCount; import org.a5calls.android.a5calls.net.FiveCallsApi; import org.a5calls.android.a5calls.R; import org.a5calls.android.a5calls.util.CustomTabsUtil; import java.text.NumberFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -48,6 +52,8 @@ public class AboutActivity extends AppCompatActivity { private static final String TAG = "AboutActivity"; public static final String KEY_DISTRICT_ID = "key_district_id"; + private static final int MIN_CALLS_TO_SHOW = 200; + private final AccountManager accountManager = AccountManager.Instance; private FiveCallsApi.CallRequestListener mStatusListener; @@ -187,10 +193,34 @@ public void onJsonError() { } @Override - public void onReportReceived(int count, boolean donateOn) { + public void onReportReceived(int count, boolean donateOn, long serverTime, + List hourlyCounts) { binding.callsToDate.setText(String.format( getResources().getString(R.string.calls_to_date), - NumberFormat.getNumberInstance(Locale.US).format(count))); + NumberFormat.getNumberInstance(Locale.getDefault()).format(count))); + if (hourlyCounts != null && !hourlyCounts.isEmpty()) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + Date localMidnight = calendar.getTime(); + + int todayCount = 0; + for (HourlyCallCount hourlyCount : hourlyCounts) { + if (!hourlyCount.getTime().before(localMidnight)) { + todayCount += hourlyCount.count; + } + } + + if (todayCount >= MIN_CALLS_TO_SHOW) { + binding.callsToday.setVisibility(VISIBLE); + binding.callsToday.setText(String.format( + getResources().getString(R.string.calls_today), + NumberFormat.getNumberInstance(Locale.getDefault()).format(todayCount))); + } + } } @Override diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/IssueActivity.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/IssueActivity.java index f65b3f62..62532e2b 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/IssueActivity.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/IssueActivity.java @@ -65,6 +65,7 @@ import org.a5calls.android.a5calls.model.Contact; import org.a5calls.android.a5calls.model.CustomizedContactScript; import org.a5calls.android.a5calls.model.DatabaseHelper; +import org.a5calls.android.a5calls.model.HourlyCallCount; import org.a5calls.android.a5calls.model.Issue; import org.a5calls.android.a5calls.model.Outcome; import org.a5calls.android.a5calls.net.FiveCallsApi; @@ -164,7 +165,8 @@ public void onJsonError() { } @Override - public void onReportReceived(int count, boolean donateOn) { + public void onReportReceived(int count, boolean donateOn, long serverTime, + List callCounts) { // unused } diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/TutorialActivity.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/TutorialActivity.java index 7a1c17d9..0ac36dd2 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/TutorialActivity.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/TutorialActivity.java @@ -28,9 +28,11 @@ import org.a5calls.android.a5calls.R; import org.a5calls.android.a5calls.databinding.ActivityTutorialBinding; import org.a5calls.android.a5calls.model.AccountManager; +import org.a5calls.android.a5calls.model.HourlyCallCount; import org.a5calls.android.a5calls.net.FiveCallsApi; import java.text.NumberFormat; +import java.util.List; import java.util.Locale; /** @@ -243,7 +245,8 @@ public void onJsonError() { } @Override - public void onReportReceived(int count, boolean donateOn) { + public void onReportReceived(int count, boolean donateOn, long serverTime, + List hourlyCounts) { if (!isAdded()) { // No longer attached to the activity! return; diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/model/HourlyCallCount.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/model/HourlyCallCount.java new file mode 100644 index 00000000..7c603668 --- /dev/null +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/model/HourlyCallCount.java @@ -0,0 +1,43 @@ +package org.a5calls.android.a5calls.model; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Date; + +public class HourlyCallCount implements Parcelable { + public long time; + public int count; + + protected HourlyCallCount(Parcel in) { + time = in.readLong(); + count = in.readInt(); + } + + public Date getTime() { + return new Date(time * 1000); + } + + public static final Creator CREATOR = new Creator() { + @Override + public HourlyCallCount createFromParcel(Parcel in) { + return new HourlyCallCount(in); + } + + @Override + public HourlyCallCount[] newArray(int size) { + return new HourlyCallCount[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(time); + dest.writeInt(count); + } +} diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/net/FiveCallsApi.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/net/FiveCallsApi.java index beee4f39..5562668a 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/net/FiveCallsApi.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/net/FiveCallsApi.java @@ -22,6 +22,7 @@ import org.a5calls.android.a5calls.model.AccountManager; import org.a5calls.android.a5calls.model.Contact; import org.a5calls.android.a5calls.model.CustomizedContactScript; +import org.a5calls.android.a5calls.model.HourlyCallCount; import org.a5calls.android.a5calls.model.Issue; import org.a5calls.android.a5calls.model.Outcome; import org.json.JSONArray; @@ -67,7 +68,8 @@ public interface CallRequestListener { void onJsonError(); - void onReportReceived(int count, boolean donateOn); + void onReportReceived(int count, boolean donateOn, long serverTime, + List hourlyCounts); void onCallReported(); } @@ -380,28 +382,30 @@ public void onErrorResponse(VolleyError error) { public void getReport() { JsonObjectRequest reportRequest = new JsonObjectRequest( - Request.Method.GET, GET_REPORT, null, new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - try { - int count = response.getInt("count"); - boolean donateOn = response.getBoolean("donateOn"); - for (CallRequestListener listener : mCallRequestListeners) { - listener.onReportReceived(count, donateOn); - } - } catch (JSONException e) { - for (CallRequestListener listener : mCallRequestListeners) { - listener.onJsonError(); + Request.Method.GET, GET_REPORT, null, response -> { + try { + int count = response.getInt("count"); + boolean donateOn = response.getBoolean("donateOn"); + long serverTime = 0; + if (response.has("serverTime")) { + serverTime = response.getLong("serverTime"); + } + JSONArray jsonArray = response.optJSONArray("hourlyCalls"); + List hourlyCounts = null; + if (jsonArray != null) { + Type listType = new TypeToken>(){}.getType(); + hourlyCounts = mGson.fromJson(jsonArray.toString(), listType); + } + for (CallRequestListener listener : mCallRequestListeners) { + listener.onReportReceived(count, donateOn, serverTime, hourlyCounts); + } + } catch (JSONException e) { + for (CallRequestListener listener : mCallRequestListeners) { + listener.onJsonError(); + } + e.printStackTrace(); } - e.printStackTrace(); - } - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onRequestError(error); - } - }); + }, this::onRequestError); reportRequest.setTag(TAG); // TODO: same tag OK? // Add the request to the RequestQueue. mRequestQueue.add(reportRequest); diff --git a/5calls/app/src/main/res/layout/activity_about.xml b/5calls/app/src/main/res/layout/activity_about.xml index 68f46682..aba7a301 100644 --- a/5calls/app/src/main/res/layout/activity_about.xml +++ b/5calls/app/src/main/res/layout/activity_about.xml @@ -48,14 +48,24 @@ android:text="@string/about_p1" /> + +