diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/adapter/IssuesAdapter.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/adapter/IssuesAdapter.java index 029d142..50f4670 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/adapter/IssuesAdapter.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/adapter/IssuesAdapter.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; import java.util.regex.Pattern; import androidx.annotation.NonNull; @@ -425,22 +426,13 @@ public void onClick(View v) { R.string.demo_previous_call_stats_one)); } else { vh.numCalls.setText(mActivity.getResources().getString( - R.string.call_count_one)); + R.string.call_count_today_one)); vh.previousCallStats.setVisibility(View.GONE); } return; } - if (mAddressErrorType != NO_ERROR) { - // If there was an address error, clear the number of calls to make - vh.numCalls.setText(""); - vh.numCalls.setVisibility(View.GONE); - vh.previousCallStats.setVisibility(View.GONE); - issue.contacts = null; - return; - } vh.numCalls.setVisibility(View.VISIBLE); - // Sometimes an issue is shown with no contact areas in order to // inform users that a major vote or change has happened. if (issue.contactAreas.isEmpty()) { @@ -450,6 +442,23 @@ public void onClick(View v) { return; } + if (mAddressErrorType != NO_ERROR) { + // If there was an address error, show generic info about the number + // of calls to make. + String contactAreaText = areasToCallsOverviewString( + mActivity.getApplicationContext(), issue.contactAreas); + if (TextUtils.isEmpty(contactAreaText)) { + vh.numCalls.setText(""); + vh.numCalls.setVisibility(View.GONE); + } else { + vh.numCalls.setText(mActivity.getResources().getString(R.string.calls_to_make, + contactAreaText)); + } + vh.previousCallStats.setVisibility(View.GONE); + issue.contacts = null; + return; + } + populateIssueContacts(issue); displayPreviousCallStats(issue, vh); } else if (type == VIEW_TYPE_EMPTY_REQUEST) { @@ -525,49 +534,67 @@ private void displayPreviousCallStats(Issue issue, IssueViewHolder vh) { .getDatabaseHelper(); // Calls ever made. int totalUserCalls = dbHelper.getTotalCallsForIssueAndContacts(issue.id, issue.contacts); + // Calls per day. + int totalDayCalls = issue.contacts.size(); - // Calls today only. - int callsLeft = issue.contacts.size(); - for (Contact contact : issue.contacts) { - if(dbHelper.hasCalledToday(issue.id, contact.id)) { - callsLeft--; - } - } if (totalUserCalls == 0) { + // If the user is somewhere like DC, which doesn't have all the contact areas, + // don't even tell them about those areas. + List actualContactAreas = new ArrayList<>(); + for (Contact contact : issue.contacts) { + actualContactAreas.add(contact.area); + } + String contactAreaText = areasToCallsOverviewString(mActivity.getApplicationContext(), + actualContactAreas); + // The user has never called on this issue before. Show a simple number of calls // text, without the word "today". vh.previousCallStats.setVisibility(View.GONE); - if (callsLeft == 1) { - vh.numCalls.setText( - mActivity.getResources().getString(R.string.call_count_one)); + if (totalDayCalls == 0) { + vh.numCalls.setText(mActivity.getResources().getString( + R.string.call_count_zero)); + } else if (totalDayCalls == 1) { + vh.numCalls.setText(mActivity.getResources().getString( + R.string.call_count_one, contactAreaText)); } else { - vh.numCalls.setText(String.format( - mActivity.getResources().getString(R.string.call_count), callsLeft)); + vh.numCalls.setText(mActivity.getResources().getString( + R.string.call_count, totalDayCalls, contactAreaText)); } - } else { - vh.previousCallStats.setVisibility(View.VISIBLE); + return; + } - // Previous call stats - if (totalUserCalls == 1) { - vh.previousCallStats.setText(mActivity.getResources().getString( - R.string.previous_call_count_one)); - } else { - vh.previousCallStats.setText( - mActivity.getResources().getString( - R.string.previous_call_count_many, totalUserCalls)); + // Calls today only. + int callsLeft = totalDayCalls; + for (Contact contact : issue.contacts) { + if(dbHelper.hasCalledToday(issue.id, contact.id)) { + callsLeft--; } + } - // Calls to make today. - if (callsLeft == 0) { - vh.numCalls.setText( - mActivity.getResources().getString(R.string.call_count_today_done)); - } else if (callsLeft == 1) { - vh.numCalls.setText( - mActivity.getResources().getString(R.string.call_count_today_one)); - } else { - vh.numCalls.setText(String.format( - mActivity.getResources().getString(R.string.call_count_today), callsLeft)); - } + // Previous call stats + vh.previousCallStats.setVisibility(View.VISIBLE); + if (totalUserCalls == 1) { + vh.previousCallStats.setText(mActivity.getResources().getString( + R.string.previous_call_count_one)); + } else { + vh.previousCallStats.setText( + mActivity.getResources().getString( + R.string.previous_call_count_many, totalUserCalls)); + } + + // Calls to make today. + if (callsLeft == 0) { + vh.numCalls.setText( + mActivity.getResources().getString(R.string.call_count_today_done)); + return; + } + + if (callsLeft == 1) { + vh.numCalls.setText(mActivity.getResources().getString( + R.string.call_count_today_one)); + } else { + vh.numCalls.setText(mActivity.getResources().getString( + R.string.call_count_today, callsLeft)); } } @@ -669,4 +696,57 @@ ArrayList sortIssuesWithMetaPriority(List issues) { return result; } + + public static String areasToCallsOverviewString(Context context, List areas) { + if (areas == null || areas.isEmpty()) { + return ""; + } + + boolean hasStateUpper = areas.contains(Contact.AREA_STATE_UPPER); + boolean hasStateLower = areas.contains(Contact.AREA_STATE_LOWER); + + Set formattedLabels = new TreeSet<>(); + for (String area : areas) { + boolean isStateArea = Contact.AREA_STATE_UPPER.equals(area) || + Contact.AREA_STATE_LOWER.equals(area); + if (isStateArea && hasStateUpper && hasStateLower) { + formattedLabels.add(context.getString(R.string.state_reps)); + } else if (isStateArea) { + formattedLabels.add(context.getString(R.string.state_rep)); + } else { + formattedLabels.add(areaToNiceString(context, area)); + } + } + + StringBuilder resultBuilder = new StringBuilder(); + boolean isFirst = true; + for (String label : formattedLabels) { + if (!isFirst) { + resultBuilder.append(", "); + } + resultBuilder.append(label); + isFirst = false; + } + + return resultBuilder.toString(); + } + + /** + * Converts an area name to a generic office name that can be used in the interface. + */ + public static String areaToNiceString(Context context, String area) { + return switch (area) { + case Contact.AREA_HOUSE -> context.getString(R.string.house_rep); + case Contact.AREA_SENATE -> context.getString(R.string.senators); + + // State legislatures call themselves different things by state, + // so let's use a generic term for all of them + case Contact.AREA_STATE_UPPER, Contact.AREA_STATE_LOWER -> context.getString(R.string.state_reps); + case Contact.AREA_GOVERNOR -> context.getString(R.string.governor); + case Contact.AREA_ATTORNEY_GENERAL -> context.getString(R.string.attorneys_general); + case Contact.AREA_SECRETARY_OF_STATE -> context.getString(R.string.secretary_of_state); + default -> area; + }; + } } + diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/model/Contact.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/model/Contact.java index 5f613bc..398c3be 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/model/Contact.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/model/Contact.java @@ -17,7 +17,7 @@ public class Contact implements Parcelable { public static final String AREA_STATE_LOWER = "StateLower"; public static final String AREA_STATE_UPPER = "StateUpper"; public static final String AREA_GOVERNOR = "Governor"; - public static final String AREA_ATTORNEY_GENERAL = "AttorneyGeneral"; + public static final String AREA_ATTORNEY_GENERAL = "AttorneysGeneral"; public static final String AREA_SECRETARY_OF_STATE = "SecretaryOfState"; // Used to show the placeholder contact for the demonstration issue. diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/util/ScriptReplacements.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/util/ScriptReplacements.java index c6f7b16..ea4326a 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/util/ScriptReplacements.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/util/ScriptReplacements.java @@ -84,7 +84,7 @@ private static Pattern wholeRegex(Pattern introPattern) { case Contact.AREA_SENATE, SENATE -> context.getString(R.string.title_us_senate); case Contact.AREA_STATE_LOWER, Contact.AREA_STATE_UPPER -> context.getString(R.string.title_state_rep); case Contact.AREA_GOVERNOR -> context.getString(R.string.title_governor); - case Contact.AREA_ATTORNEY_GENERAL -> context.getString(R.string.title_attorney_general); + case Contact.AREA_ATTORNEY_GENERAL -> context.getString(R.string.title_attorneys_general); case Contact.AREA_SECRETARY_OF_STATE -> context.getString(R.string.title_secretary_of_state); default -> null; }; diff --git a/5calls/app/src/main/res/values-es/strings.xml b/5calls/app/src/main/res/values-es/strings.xml index 0703c18..1b281cf 100644 --- a/5calls/app/src/main/res/values-es/strings.xml +++ b/5calls/app/src/main/res/values-es/strings.xml @@ -16,10 +16,13 @@ Error: Datos inválidos o inexistentes para esta ubicación - %1$d llamadas por hacer + %1$d llamadas: %2$s + + + 0 llamadas por hacer - 1 llamada por hacer + 1 llamada: %1$s %1$d llamadas por hacer hoy @@ -527,6 +530,18 @@ Política de privacidad + + Representante + Senadores + Representantes Estatales + Representante Estatal + Gobernador + Fiscal General + Secretario de Estado + + + Llamar a %1$s + Obtenga actualizaciones sobre temas más recientes diff --git a/5calls/app/src/main/res/values/strings.xml b/5calls/app/src/main/res/values/strings.xml index 800d054..d890ae8 100644 --- a/5calls/app/src/main/res/values/strings.xml +++ b/5calls/app/src/main/res/values/strings.xml @@ -16,10 +16,13 @@ Error: Invalid or no data for this location. Try again later. - %1$d calls to make + %1$d calls: %2$s + + + 0 calls to make - 1 call to make + 1 call: %1$s %1$d calls to make today @@ -626,9 +629,21 @@ Senator Legislator Governor - Attorney General + Attorney General Secretary of State + + House Rep + Senators + State Reps + State Rep + Governor + Attorney General + Secretary of State + + + Call %1$s + Get updates on the latest issues diff --git a/5calls/app/src/main/res/values/styles.xml b/5calls/app/src/main/res/values/styles.xml index 73c4cba..6da6d0f 100644 --- a/5calls/app/src/main/res/values/styles.xml +++ b/5calls/app/src/main/res/values/styles.xml @@ -290,6 +290,7 @@ diff --git a/5calls/app/src/test/java/org/a5calls/android/a5calls/adapter/IssuesAdapterTest.java b/5calls/app/src/test/java/org/a5calls/android/a5calls/adapter/IssuesAdapterTest.java index 8753f6d..169e767 100644 --- a/5calls/app/src/test/java/org/a5calls/android/a5calls/adapter/IssuesAdapterTest.java +++ b/5calls/app/src/test/java/org/a5calls/android/a5calls/adapter/IssuesAdapterTest.java @@ -1,5 +1,7 @@ package org.a5calls.android.a5calls.adapter; +import android.content.Context; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; @@ -7,18 +9,21 @@ import org.a5calls.android.a5calls.FakeJSONData; import org.a5calls.android.a5calls.model.Contact; import org.a5calls.android.a5calls.model.Issue; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -29,6 +34,14 @@ @RunWith(AndroidJUnit4.class) public class IssuesAdapterTest { + private Context realContext; + + @Before + public void setUp() { + // Grab the actual Context from the test application + realContext = ApplicationProvider.getApplicationContext(); + } + @Test public void testFilterIssuesBySearchText_noMatches() { List issues = issuesFromJson(FakeJSONData.ISSUE_DATA); @@ -572,6 +585,64 @@ public void testFilterBookmarkedIssues_nonExistentIdsIgnored() { assertEquals("100", filtered.get(0).id); } + @Test + public void testFormatAreaLabels_nullInput_returnsEmptyString() { + assertEquals("", IssuesAdapter.areasToCallsOverviewString(realContext, null)); + } + + @Test + public void testFormatAreaLabels_emptyInput_returnsEmptyString() { + List input = Collections.emptyList(); + assertEquals("", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_singleStandardArea_returnsLocalized() { + List input = Collections.singletonList("US House"); + assertEquals("House Rep", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_singleStateArea_returnsSingularStateRep() { + // Only one of the state chambers is present + List input = Collections.singletonList("StateUpper"); + assertEquals("State Rep", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_bothStateAreas_returnsPluralStateReps() { + // Both chambers are present, triggering the plural logic and deduplication + List input = Arrays.asList("StateUpper", "StateLower"); + assertEquals("State Reps", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_complexList_sortsAndJoinsCorrectly() { + // Mix of different types to ensure mapping, sorting, and joining work together + List input = Arrays.asList("Governor", "US House", "StateUpper"); + + // Expected mapping: "Governor", "House Rep", "State Rep" + // Expected alphabetical sorting: "Governor", "House Rep", "State Rep" + assertEquals("Governor, House Rep, State Rep", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_duplicatesCombined_sortsCorrectly() { + // Testing the specific scenario you pointed out earlier + List input = Arrays.asList("StateUpper", "StateLower", "Governor"); + + // Expected deduplication: "State Reps", "Governor" + // Expected alphabetical sorting: "Governor", "State Reps" + assertEquals("Governor, State Reps", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + + @Test + public void testFormatAreaLabels_unknownArea_returnsRawString() { + // If an area isn't in our switch statement, it should just pass the raw string through + List input = Arrays.asList("City Council", "Governor"); + assertEquals("City Council, Governor", IssuesAdapter.areasToCallsOverviewString(realContext, input)); + } + private List issuesFromJson(String json) { Gson gson = new GsonBuilder().serializeNulls().create(); Type listType = new TypeToken>(){}.getType();