Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import android.content.Context;
import android.view.View;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.volley.toolbox.HttpResponse;
import com.google.android.material.appbar.CollapsingToolbarLayout;

import org.a5calls.android.a5calls.AppSingleton;
import org.a5calls.android.a5calls.FakeJSONData;
import org.a5calls.android.a5calls.FiveCallsApplication;
import org.a5calls.android.a5calls.R;
import org.a5calls.android.a5calls.model.AccountManager;
import org.a5calls.android.a5calls.model.DatabaseHelper;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
Expand All @@ -30,6 +34,7 @@
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withInputType;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.test.platform.app.InstrumentationRegistry;
Expand Down Expand Up @@ -70,7 +75,7 @@ public void describeTo(Description description) {
/**
* Sets up mock responses for API calls
*/
private void setupMockResponses(boolean isSplit, boolean hasLocation) {
private void setupMockResponses(boolean isSplit, boolean hasLocation) {
// Set up the mock to handle all possible requests with appropriate responses
mHttpStack.clearUrlPatternResponses();

Expand Down Expand Up @@ -120,8 +125,8 @@ public void testMainUILoadsCorrectly() throws JSONException {

// Check that the collapsing toolbar is displayed and contains the location text
onView(withId(R.id.collapsing_toolbar))
.check(matches(isDisplayed()))
.check(matches(withCollapsingToolbarTitle(containsString("BOWLING GREEN"))));
.check(matches(isDisplayed()))
.check(matches(withCollapsingToolbarTitle(containsString("BOWLING GREEN"))));

// Check that no location error was shown.
onView(withText(R.string.low_accuracy_warning)).check(doesNotExist());
Expand Down Expand Up @@ -163,10 +168,122 @@ public void testMainUILoadsCorrectly_NoLocation() {
// Verify that a "set your location" button is displayed.
onView(withContentDescription(R.string.first_location_title)).check(matches(isDisplayed()));

// No calls to make displayed.
onView(withText("3 calls to make")).check(doesNotExist());
onView(withText("2 calls to make")).check(doesNotExist());

// Set the address again for the sake of the next test.
AccountManager.Instance.setAddress(context, address);
}

@Test
public void testMainUILoadsCorrectly_noLocation_placeholderShown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Clear the location.
String address = AccountManager.Instance.getAddress(context);
AccountManager.Instance.setAddress(context, "");
setupMockResponses(/*isSplit=*/false, /*hasLocation=*/false);
setupMockRequestQueue();

launchMainActivity(1000);

// Verify that the demo issue is displayed.
onView(withText(R.string.demo_issue_name)).check(matches(isDisplayed()));
// There should be a "1 call to make" note for the demo issue.
onView(withText(R.string.call_count_one)).check(matches(isDisplayed()));

// Reset address.
AccountManager.Instance.setAddress(context, address);
}

@Test
public void testMainUILoadCorrectly_oneCall_placeholderShown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
setupMockResponses(/*isSplit=*/false, /*hasLocation=*/true);
setupMockRequestQueue();
DatabaseHelper databaseHelper = AppSingleton.getInstance(context).getDatabaseHelper();
// Add one fake call.
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");

launchMainActivity(1000);

// Verify that the demo issue is displayed.
onView(withText(R.string.demo_issue_name)).check(matches(isDisplayed()));
// There should be a "1 call to make" note for the demo issue.
onView(withText(R.string.call_count_one)).check(matches(isDisplayed()));

// Reset the database.
databaseHelper.getWritableDatabase().delete("UserCallsDatabase", null, null);
}

@Test
public void testMainUILoadCorrectly_twoCalls_placeholderNotShown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
setupMockResponses(/*isSplit=*/false, /*hasLocation=*/true);
setupMockRequestQueue();
DatabaseHelper databaseHelper = AppSingleton.getInstance(context).getDatabaseHelper();
// Add two fake calls.
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");

launchMainActivity(1000);

// Verify that the demo issue is not displayed.
onView(withText(R.string.demo_issue_name)).check(doesNotExist());

// Reset the database.
databaseHelper.getWritableDatabase().delete("UserCallsDatabase", null, null);
}

@Test
public void testMainUILoadCorrectly_fourCalls_placeholderPrefSet_placeholderShown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
setupMockResponses(/*isSplit=*/false, /*hasLocation=*/true);
setupMockRequestQueue();
DatabaseHelper databaseHelper = AppSingleton.getInstance(context).getDatabaseHelper();
// Add four fake calls.
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");
databaseHelper.addCall("issueId", "issueName", "contactId", "contactName", "result", "location");
// Pretend the user has turned on the placeholder anyway in settings.
AccountManager.Instance.setShowPlaceholderIssue(context, true);

launchMainActivity(1000);

// Verify that the demo issue is displayed with "one call to make".
onView(withText(R.string.demo_issue_name)).check(matches(isDisplayed()));
onView(withText(R.string.demo_previous_call_stats_one)).check(doesNotExist());
onView(withText(R.string.call_count_one)).check(matches(isDisplayed()));

// Reset the database.
databaseHelper.getWritableDatabase().delete("UserCallsDatabase", null, null);
AccountManager.Instance.setShowPlaceholderIssue(context, false);
}

@Test
public void testMainUILoadCorrectly_placeholderAlreadyCalled_placeholderPrefSet_placeholderShown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
setupMockResponses(/*isSplit=*/false, /*hasLocation=*/true);
setupMockRequestQueue();
// The user has already done the placeholder call once.
AccountManager.Instance.setPlaceholderIssueCalled(context, true);
// Pretend the user has turned on the placeholder anyway in settings.
AccountManager.Instance.setShowPlaceholderIssue(context, true);

launchMainActivity(1000);

// Verify that the demo issue is displayed.
onView(withText(R.string.demo_issue_name)).check(matches(isDisplayed()));
// The "one pretend previous call" is shown.
onView(withText(R.string.demo_previous_call_stats_one)).check(matches(isDisplayed()));

// Reset state
AccountManager.Instance.setPlaceholderIssueCalled(context, false);
AccountManager.Instance.setShowPlaceholderIssue(context, false);
}


@Test
public void testNavigationDrawerOpens() throws JSONException {
setupMockResponses(/*isSplit=*/ false, /*hasLocation=*/true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import org.a5calls.android.a5calls.AppSingleton;
import org.a5calls.android.a5calls.R;
import org.a5calls.android.a5calls.model.AccountManager;
import org.a5calls.android.a5calls.model.Category;
import org.a5calls.android.a5calls.model.Contact;
import org.a5calls.android.a5calls.model.DatabaseHelper;
Expand Down Expand Up @@ -270,7 +271,8 @@ public void populateIssueContacts(Issue issue) {
* This is normally done in onBindViewHolder, but is needed for deep linking
* where we bypass the RecyclerView.
*/
public static void populateIssueContacts(Issue issue, List<Contact> contacts, boolean isSplitDistrict) {
public static void populateIssueContacts(Issue issue, List<Contact> contacts,
boolean isSplitDistrict) {
if (issue == null || issue.contactAreas.isEmpty()) {
return;
}
Expand Down Expand Up @@ -335,6 +337,21 @@ public void onClick(View v) {
}
});

if (issue.isPlaceholder) {
vh.numCalls.setVisibility(View.VISIBLE);
if (AccountManager.Instance.getPlaceholderIssueCalled(mActivity)) {
vh.numCalls.setVisibility(View.GONE);
vh.previousCallStats.setVisibility(View.VISIBLE);
vh.previousCallStats.setText(mActivity.getResources().getString(
R.string.demo_previous_call_stats_one));
} else {
vh.numCalls.setText(mActivity.getResources().getString(
R.string.call_count_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("");
Expand Down Expand Up @@ -527,28 +544,34 @@ public EmptySearchViewHolder(View itemView) {

/**
* Sorts a list of issues to prioritize those with meta values (state abbreviations) at the top,
* then sorts the remaining issues. Both groups maintain their internal sort order.
* then sorts the remaining issues. Both groups maintain their internal sort order. Placeholder
* issues are always put at the very top.
*/
@VisibleForTesting
ArrayList<Issue> sortIssuesWithMetaPriority(List<Issue> issues) {
ArrayList<Issue> placeholders = new ArrayList<>();
ArrayList<Issue> withMeta = new ArrayList<>();
ArrayList<Issue> withoutMeta = new ArrayList<>();

// Separate issues with and without meta values
for (Issue issue : issues) {
if (!TextUtils.isEmpty(issue.meta)) {
if (issue.isPlaceholder) {
placeholders.add(issue);
} else if (!TextUtils.isEmpty(issue.meta)) {
withMeta.add(issue);
} else {
withoutMeta.add(issue);
}
}

// Sort each group independently by sort field (maintaining consistent order)
Collections.sort(placeholders, (a, b) -> Integer.compare(a.sort, b.sort));
Collections.sort(withMeta, (a, b) -> Integer.compare(a.sort, b.sort));
Collections.sort(withoutMeta, (a, b) -> Integer.compare(a.sort, b.sort));

// Combine: meta issues first, then regular issues
ArrayList<Issue> result = new ArrayList<>();
result.addAll(placeholders);
result.addAll(withMeta);
result.addAll(withoutMeta);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ public class IssueActivity extends AppCompatActivity implements FiveCallsApi.Scr

public static final int RESULT_OK = 1;
public static final int RESULT_SERVER_ERROR = 2;
public static final int RESULT_DEMO_CALLED = 3;

private static final String DONATE_URL = "https://secure.actblue.com/donate/5calls-donate?refcode=android&refcode2=";

private static final int MIN_CALLS_TO_SHOW_CALL_STATS = 10;

private boolean mShowServerError = false;
private boolean mShowPlaceholderCalled = false;

private Issue mIssue;
private String mAddress;
Expand Down Expand Up @@ -115,6 +117,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
if (result.getResultCode() == RESULT_SERVER_ERROR) {
mShowServerError = true;
}
if (result.getResultCode() == RESULT_DEMO_CALLED) {
mShowPlaceholderCalled = true;
}
});

FiveCallsApi api = AppSingleton.getInstance(this).getJsonController();
Expand Down Expand Up @@ -350,6 +355,10 @@ protected void onResume() {
binding.noContactAreas.setVisibility(View.VISIBLE);
return;
}
if (mShowPlaceholderCalled) {
binding.placeholderDone.getRoot().setVisibility(View.VISIBLE);
AccountManager.Instance.setShowPlaceholderIssue(getApplicationContext(), false);
}
showContactsUi();
}

Expand Down Expand Up @@ -583,7 +592,7 @@ private void populateRepView(View repView, Contact contact, final int index,
} else {
contactReason.setVisibility(View.GONE);
}
if (!contact.isPlaceholder) {
if (!contact.isPlaceholder || contact.area.equals(Contact.AREA_DEMO)) {
if (!TextUtils.isEmpty(contact.photoURL)) {
Glide.with(getApplicationContext())
.load(contact.photoURL)
Expand All @@ -593,7 +602,7 @@ private void populateRepView(View repView, Contact contact, final int index,
.into(repImage);
}
// Show a bit about whether they've been contacted yet today.
if (hasCalledToday) {
if (hasCalledToday || (mIssue.isPlaceholder && mShowPlaceholderCalled)) {
contactChecked.setImageLevel(1);
contactChecked.setContentDescription(getResources().getString(
R.string.contact_done_img_description));
Expand Down Expand Up @@ -676,6 +685,9 @@ private void showIssueDetails() {
@VisibleForTesting
static String getIssueDetailsMessage(Context context, Issue issue) {
StringBuilder result = new StringBuilder();
if (issue.isPlaceholder) {
return context.getResources().getString(R.string.demo_issue_details_message);
}
if (issue.categories.length > 0) {
if (issue.categories.length == 1) {
result.append(context.getResources().getString(R.string.issue_category_one));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public class MainActivity extends AppCompatActivity implements IssuesAdapter.Cal
private static final String KEY_SHOW_LOW_ACCURACY_WARNING = "showLowAccuracyWarning";
private static final String DEEP_LINK_HOST = "5calls.org";
private static final String DEEP_LINK_PATH_ISSUE = "issue";
private static final String DEMO_ISSUE_PERMALINK = "/issue/demoIssue";
private static final int MAX_CALLS_FOR_DEMO = 1;
private final AccountManager accountManager = AccountManager.Instance;

private String mPendingDeepLinkPath = null;
Expand All @@ -93,6 +95,7 @@ public class MainActivity extends AppCompatActivity implements IssuesAdapter.Cal
private boolean mShowLowAccuracyWarning = true;
private boolean mDonateIsOn = false;
private FirebaseAuth mAuth = null;
private int mCallCount = 0;

private ActivityMainBinding binding;

Expand Down Expand Up @@ -461,6 +464,8 @@ public void onJsonError() {
@Override
public void onIssuesReceived(List<Issue> issues) {
populateFilterAdapterIfNeeded(issues);

maybeAddPlaceholderIssue(issues);
mIssuesAdapter.setAllIssues(issues, IssuesAdapter.NO_ERROR);
mIssuesAdapter.setFilterAndSearch(mFilterText, mSearchText);
binding.swipeContainer.setRefreshing(false);
Expand Down Expand Up @@ -653,12 +658,12 @@ public void onNothingSelected(AdapterView<?> adapterView) {
}

private void loadStats() {
int callCount = AppSingleton.getInstance(getApplicationContext())
mCallCount = AppSingleton.getInstance(getApplicationContext())
.getDatabaseHelper().getCallsCount();
if (callCount > 1) {
if (mCallCount > 1) {
// Don't bother if it is less than 1.
binding.actionBarSubtitle.setText(String.format(
getResources().getString(R.string.your_call_count_summary), callCount));
getResources().getString(R.string.your_call_count_summary), mCallCount));
}
}

Expand Down Expand Up @@ -849,4 +854,28 @@ private void maybeHandlePendingDeepLink() {
showSnackbar(R.string.issue_not_found, Snackbar.LENGTH_LONG);
}
}

private void maybeAddPlaceholderIssue(List<Issue> issues) {
// Option to force show the placeholder call in settings.
boolean forceShowPlaceholder = AccountManager.Instance.showPlaceholderIssue(
getApplicationContext());
// If they've called more than N times, don't bother with the placeholder any more.
boolean showPlaceholderIfEarly = mCallCount <= MAX_CALLS_FOR_DEMO &&
!accountManager.getPlaceholderIssueCalled(getApplicationContext());
if (forceShowPlaceholder || showPlaceholderIfEarly) {
Contact demoContact = Contact.createPlaceholder("0",
getResources().getString(R.string.demo_rep_name),
getResources().getString(R.string.demo_rep_reason),
Contact.AREA_DEMO,
getResources().getString(R.string.demo_rep_phone));
Issue demoIssue = Issue.createPlaceholder("0",
getResources().getString(R.string.demo_issue_name),
DEMO_ISSUE_PERMALINK,
getResources().getString(R.string.demo_issue_reason),
getResources().getString(R.string.demo_issue_script), true, 0,
Collections.singletonList(demoContact),
Collections.emptyList(), Collections.emptyList());
issues.add(demoIssue);
}
}
}
Loading
Loading