diff --git a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/MainActivityHappyPathTest.java b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/MainActivityHappyPathTest.java index 48816983..0e50289d 100644 --- a/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/MainActivityHappyPathTest.java +++ b/5calls/app/src/androidTest/java/org/a5calls/android/a5calls/controller/MainActivityHappyPathTest.java @@ -1,5 +1,6 @@ package org.a5calls.android.a5calls.controller; +import android.content.Context; import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -8,6 +9,7 @@ import org.a5calls.android.a5calls.FakeJSONData; import org.a5calls.android.a5calls.R; +import org.a5calls.android.a5calls.model.AccountManager; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; @@ -20,14 +22,23 @@ import java.util.ArrayList; import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; 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; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertTrue; /** * Integration test for MainActivity that tests the happy path. @@ -59,23 +70,25 @@ public void describeTo(Description description) { /** * Sets up mock responses for API calls */ - private void setupMockResponses(boolean isSplit) { + private void setupMockResponses(boolean isSplit, boolean hasLocation) { + // Set up the mock to handle all possible requests with appropriate responses + mHttpStack.clearUrlPatternResponses(); + // Use FakeJSONData for mock issues response JSONArray issuesArray = FakeJSONData.getIssueJSON(); HttpResponse issuesResponse = new HttpResponse(200, new ArrayList<>(), issuesArray.toString().getBytes()); + mHttpStack.setResponseForUrlPattern("issues", issuesResponse); // Use FakeJSONData for mock contacts response - JSONObject contactsResponseJson = FakeJSONData.getRepsJSON(isSplit); - HttpResponse contactsResponse = new HttpResponse(200, new ArrayList<>(), contactsResponseJson.toString().getBytes()); + if (hasLocation) { + JSONObject contactsResponseJson = FakeJSONData.getRepsJSON(isSplit); + HttpResponse contactsResponse = new HttpResponse(200, new ArrayList<>(), contactsResponseJson.toString().getBytes()); + mHttpStack.setResponseForUrlPattern("reps", contactsResponse); + } // Use FakeJSONData for mock report response JSONObject reportResponseJson = FakeJSONData.getReportJSON(); HttpResponse reportResponse = new HttpResponse(200, new ArrayList<>(), reportResponseJson.toString().getBytes()); - - // Set up the mock to handle all possible requests with appropriate responses - mHttpStack.clearUrlPatternResponses(); - mHttpStack.setResponseForUrlPattern("issues", issuesResponse); - mHttpStack.setResponseForUrlPattern("reps", contactsResponse); mHttpStack.setResponseForUrlPattern("report", reportResponse); // Set a default response for any other requests @@ -84,12 +97,15 @@ private void setupMockResponses(boolean isSplit) { @Test public void testMainUILoadsCorrectly() throws JSONException { - setupMockResponses(/*isSplit=*/false); + setupMockResponses(/*isSplit=*/false, /*hasLocation=*/true); setupMockRequestQueue(); launchMainActivity(1000); + // Verify the location placeholder in the header is not shown. + onView(withContentDescription("5 Calls for BOWLING GREEN")).check(matches(isDisplayed())); + // Verify that the toolbar is displayed onView(withId(R.id.toolbar)).check(matches(isDisplayed())); @@ -112,8 +128,30 @@ public void testMainUILoadsCorrectly() throws JSONException { } @Test - public void testMainUILoadsCorrectly_SplitWarning() throws JSONException { - setupMockResponses(/*isSplit=*/true); + public void testMainUILoadsCorrectly_SplitWarning() { + setupMockResponses(/*isSplit=*/true, /*hasLocation=*/true); + + setupMockRequestQueue(); + + launchMainActivity(1000); + + // Verify that a real issue is displayed (using the first issue from the real data) + onView(withText("Condemn a US Takeover of Gaza")).check(matches(isDisplayed())); + + // Check that the location error was shown that is specific to split districts. + onView(withText(R.string.split_district_warning)).check(matches(isDisplayed())); + + // No button to set location is shown because some location was set. + onView(withContentDescription(R.string.first_location_title)).check(matches(not(isDisplayed()))); + } + + @Test + public void testMainUILoadsCorrectly_NoLocation() { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + // Clear the location. + String address = AccountManager.Instance.getAddress(context); + AccountManager.Instance.setAddress(context, ""); + setupMockResponses(/*isSplit=*/false, /*hasLocation=*/false); setupMockRequestQueue(); @@ -122,13 +160,16 @@ public void testMainUILoadsCorrectly_SplitWarning() throws JSONException { // Verify that a real issue is displayed (using the first issue from the real data) onView(withText("Condemn a US Takeover of Gaza")).check(matches(isDisplayed())); - // Check that the location error was shown. - onView(withText(R.string.low_accuracy_warning)).check(matches(isDisplayed())); + // Verify that a "set your location" button is displayed. + onView(withContentDescription(R.string.first_location_title)).check(matches(isDisplayed())); + + // Set the address again for the sake of the next test. + AccountManager.Instance.setAddress(context, address); } @Test public void testNavigationDrawerOpens() throws JSONException { - setupMockResponses(/*isSplit=*/ false); + setupMockResponses(/*isSplit=*/ false, /*hasLocation=*/true); setupMockRequestQueue(); @@ -163,4 +204,47 @@ public void testNavigationDrawerOpens() throws JSONException { onView(withText("FAQ")).check(matches(isDisplayed())); onView(withText("Update location")).check(matches(isDisplayed())); } + + @Test + // TODO: Consider moving to a tutorial-specific test file. + public void MainActivity_ShowsTutorialOnce() throws InterruptedException { + // Mark tutorial as not seen yet. + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + AccountManager.Instance.setTutorialSeen(context, false); + // Clear the location. + String address = AccountManager.Instance.getAddress(context); + AccountManager.Instance.setAddress(context, ""); + + // Now we are in the state of starting the app for the first time. + + setupMockResponses(/*isSplit=*/ false, /*hasLocation=*/true); + setupMockRequestQueue(); + + launchMainActivity(1000); + + // First tutorial screen shown. + onView(withText(R.string.about_p2)).check(matches(isDisplayed())); + onView(allOf(withText(R.string.next), isDisplayed())).perform(click()); + + // Second tutorial screen shown. + onView(withText(R.string.about_p2_2)).check(matches(isDisplayed())); + onView(allOf(withText(R.string.next), isDisplayed())).perform(click()); + + // Third tutorial screen shown. + onView(withText(R.string.about_splash_3)).check(matches(isDisplayed())); + onView(allOf(withText(R.string.get_started_btn), isDisplayed())).perform(click()); + + // Location screen shown. + onView(withText(R.string.location_prompt)).check(matches(isDisplayed())); + onView(withText(R.string.skip_location_btn)).perform(click()); + + // When we reach main activity, the tutorial is seen and the button + // to set location is shown. + Thread.sleep(1000); + onView(withContentDescription(R.string.first_location_title)).check(matches(isDisplayed())); + assertTrue(AccountManager.Instance.isTutorialSeen(context)); + + // Put the address back. + AccountManager.Instance.setAddress(context, address); + } } \ No newline at end of file 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 96bc1b7f..a2298ed4 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 @@ -239,6 +239,10 @@ public boolean hasContacts() { return !mContacts.isEmpty(); } + public boolean hasAddressError() { + return mAddressErrorType != NO_ERROR; + } + public List getAllIssues() { return mAllIssues; } @@ -249,16 +253,29 @@ public List getAllIssues() { * where we bypass the RecyclerView. */ public void populateIssueContacts(Issue issue) { + if (mAddressErrorType == ERROR_ADDRESS) { + issue.contacts = null; + return; + } + populateIssueContacts(issue, mContacts, mIsSplitDistrict); + } + + /** + * Populates an issue's contacts list based on its contact areas. + * This is normally done in onBindViewHolder, but is needed for deep linking + * where we bypass the RecyclerView. + */ + public static void populateIssueContacts(Issue issue, List contacts, boolean isSplitDistrict) { if (issue == null || issue.contactAreas.isEmpty()) { return; } - issue.contacts = new ArrayList(); + issue.contacts = new ArrayList<>(); for (String contactArea : issue.contactAreas) { - for (Contact contact : mContacts) { + for (Contact contact : contacts) { if (TextUtils.equals(contact.area, contactArea) && !issue.contacts.contains(contact)) { - if (TextUtils.equals(contact.area, Contact.AREA_HOUSE) && mIsSplitDistrict) { + if (TextUtils.equals(contact.area, Contact.AREA_HOUSE) && isSplitDistrict) { issue.isSplit = true; } issue.contacts.add(contact); @@ -321,9 +338,12 @@ public void onClick(View v) { 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. 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 61158c16..633b119e 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 @@ -47,6 +47,7 @@ import org.a5calls.android.a5calls.BuildConfig; import org.a5calls.android.a5calls.FiveCallsApplication; import org.a5calls.android.a5calls.R; +import org.a5calls.android.a5calls.adapter.IssuesAdapter; import org.a5calls.android.a5calls.databinding.ActivityIssueBinding; import org.a5calls.android.a5calls.model.AccountManager; import org.a5calls.android.a5calls.model.Contact; @@ -86,6 +87,8 @@ public class IssueActivity extends AppCompatActivity implements FiveCallsApi.Scr private boolean mShowServerError = false; private Issue mIssue; + private String mAddress; + private String mLocationName; // indicates that the zip entered intersects with multiple congressional districts private boolean mIsDistrictSplit = false; // low accuracy locations are zip codes or city names, we warn on state reps if you are using one @@ -95,6 +98,9 @@ public class IssueActivity extends AppCompatActivity implements FiveCallsApi.Scr private ActivityIssueBinding binding; private ActivityResultLauncher mRepCallLauncher; + private ActivityResultLauncher mLocationLauncher; + + private FiveCallsApi.ContactsRequestListener mContactsRequestListener; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -111,15 +117,69 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } }); + FiveCallsApi api = AppSingleton.getInstance(this).getJsonController(); + mLocationLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + // If the address has changed after going to the location activity, refresh + // the contacts for this issue. + String newAddress = MainActivity.getLocationString(getApplicationContext()); + if (!TextUtils.equals(newAddress, mAddress)) { + mAddress = newAddress; + api.getContacts(mAddress); + } + } + ); + mContactsRequestListener = new FiveCallsApi.ContactsRequestListener() { + + @Override + public void onRequestError() { + Snackbar.make(getWindow().getDecorView(), + getResources().getString(R.string.request_error), + Snackbar.LENGTH_LONG).show(); + // Do not change the UI. + } + + @Override + public void onJsonError() { + Snackbar.make(getWindow().getDecorView(), + getResources().getString(R.string.json_error), + Snackbar.LENGTH_LONG).show(); + // Do not change the UI. + } + + @Override + public void onAddressError() { + Snackbar.make(getWindow().getDecorView(), + getResources().getString(R.string.error_address_invalid), + Snackbar.LENGTH_LONG).show(); + mIssue.contacts = null; + showContactsUi(); + } + + @Override + public void onContactsReceived(String locationName, String districtId, boolean isDistrictSplit, boolean isLowAccuracy, List contacts, boolean stateChanged) { + mIsDistrictSplit = isDistrictSplit; + mIsLowAccuracy = isLowAccuracy; + mLocationName = locationName; + IssuesAdapter.populateIssueContacts(mIssue, contacts, isDistrictSplit); + showContactsUi(); + } + }; + api.registerContactsRequestListener(mContactsRequestListener); + mIssue = getIntent().getParcelableExtra(KEY_ISSUE); if (mIssue == null) { // TODO handle this better? Is it even possible to get here? finish(); return; } + + mAddress = getIntent().getStringExtra(RepCallActivity.KEY_ADDRESS); mIsDistrictSplit = getIntent().getBooleanExtra(KEY_IS_DISTRICT_SPLIT, false); mIsLowAccuracy = getIntent().getBooleanExtra(KEY_IS_LOW_ACCURACY, false); mDonateIsOn = getIntent().getBooleanExtra(KEY_DONATE_IS_ON, false); + mLocationName = getIntent().getStringExtra(RepCallActivity.KEY_LOCATION_NAME); setContentView(binding.getRoot()); @@ -153,6 +213,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { child.setPadding(repItemHorizontalPadding + insets.left, repItemVerticalPadding, repItemHorizontalPadding + insets.right, repItemVerticalPadding); } + final int errorSectionVerticalPadding = getResources().getDimensionPixelSize(R.dimen.activity_horizontal_margin); + binding.noCallsLeft.setPadding(activityPadding + insets.left, errorSectionVerticalPadding, activityPadding+insets.right, errorSectionVerticalPadding); + binding.noAddressSet.setPadding(activityPadding + insets.left, errorSectionVerticalPadding, activityPadding+insets.right, errorSectionVerticalPadding); + binding.noContactAreas.setPadding(activityPadding + insets.left, errorSectionVerticalPadding, activityPadding+insets.right, errorSectionVerticalPadding); + behavior.setPeekHeight(targetPeakHeight + insets.bottom); return WindowInsetsCompat.CONSUMED; }); @@ -250,7 +315,6 @@ public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, FiveCallsApplication.analyticsManager().trackPageview(mIssue.permalink, this); // Register scripts request listener once - FiveCallsApi api = AppSingleton.getInstance(this).getJsonController(); api.registerScriptsRequestListener(this); // Fetch customized scripts once on create @@ -286,14 +350,31 @@ protected void onResume() { binding.noContactAreas.setVisibility(View.VISIBLE); return; } + showContactsUi(); + } + + private void showContactsUi() { + binding.noAddressSet.setVisibility(View.GONE); + binding.noCallsLeft.setVisibility(View.GONE); if (mIssue.contacts == null) { - // Probably an address error. - binding.noCallsLeft.setVisibility(View.VISIBLE); + if (AccountManager.Instance.hasLocation(this)) { + // An address error. + binding.noCallsLeft.setVisibility(View.VISIBLE); + binding.updateLocationButton.setOnClickListener(view -> { + Intent intent = new Intent(IssueActivity.this, LocationActivity.class); + intent.putExtra(LocationActivity.ALLOW_HOME_UP_KEY, true); + mLocationLauncher.launch(intent); + }); + } else { + // Hasn't set an address yet. + binding.noAddressSet.setVisibility(View.VISIBLE); + binding.setLocationButton.setOnClickListener(view -> { + Intent intent = new Intent(IssueActivity.this, LocationActivity.class); + intent.putExtra(LocationActivity.ALLOW_HOME_UP_KEY, true); + mLocationLauncher.launch(intent); + }); + } binding.issueDone.getRoot().setVisibility(View.GONE); - binding.updateLocationButton.setOnClickListener(view -> { - Intent intent = new Intent(IssueActivity.this, LocationActivity.class); - startActivity(intent); - }); return; } @@ -307,7 +388,7 @@ protected void onResume() { senateCount++; } } - if (senateCount == 1) { + if (senateCount <= 1) { mIssue.contacts.add(Contact.createPlaceholder( "placeholderSenate", getResources().getString(R.string.vacant_seat_rep_name), @@ -343,6 +424,7 @@ protected void onResume() { @Override public void onClick(View view) { Intent intent = new Intent(IssueActivity.this, LocationActivity.class); + intent.putExtra(LocationActivity.ALLOW_HOME_UP_KEY, true); startActivity(intent); } }); @@ -525,10 +607,8 @@ private void populateRepView(View repView, Contact contact, final int index, public void onClick(View view) { Intent intent = new Intent(getApplicationContext(), RepCallActivity.class); intent.putExtra(KEY_ISSUE, mIssue); - intent.putExtra(RepCallActivity.KEY_ADDRESS, - getIntent().getStringExtra(RepCallActivity.KEY_ADDRESS)); - intent.putExtra(RepCallActivity.KEY_LOCATION_NAME, - getIntent().getStringExtra(RepCallActivity.KEY_LOCATION_NAME)); + intent.putExtra(RepCallActivity.KEY_ADDRESS, mAddress); + intent.putExtra(RepCallActivity.KEY_LOCATION_NAME, mLocationName); intent.putExtra(RepCallActivity.KEY_ACTIVE_CONTACT_INDEX, index); mRepCallLauncher.launch(intent); } @@ -645,10 +725,9 @@ private void fetchCustomizedScripts() { return; } - String address = getIntent().getStringExtra(RepCallActivity.KEY_ADDRESS); - String locationName = getIntent().getStringExtra(RepCallActivity.KEY_LOCATION_NAME); + String locationName = mLocationName; - if (TextUtils.isEmpty(address) && TextUtils.isEmpty(locationName)) { + if (TextUtils.isEmpty(mAddress) && TextUtils.isEmpty(locationName)) { return; } @@ -660,7 +739,8 @@ private void fetchCustomizedScripts() { FiveCallsApi api = AppSingleton.getInstance(this).getJsonController(); String userName = AccountManager.Instance.getUserName(this); - api.getCustomizedScripts(mIssue.id, contactIds, locationName != null ? locationName : address, userName); + api.getCustomizedScripts(mIssue.id, contactIds, + locationName != null ? locationName : mAddress, userName); } @Override @@ -685,5 +765,6 @@ protected void onDestroy() { super.onDestroy(); FiveCallsApi api = AppSingleton.getInstance(this).getJsonController(); api.unregisterScriptsRequestListener(this); + api.unregisterContactsRequestListener(mContactsRequestListener); } } diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/LocationActivity.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/LocationActivity.java index 91da12e3..d1259d45 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/LocationActivity.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/LocationActivity.java @@ -86,17 +86,23 @@ protected void onCreate(Bundle savedInstanceState) { // Allow home up if required. Intent intent = getIntent(); if (intent != null) { + ActionBar supportActionBar = getSupportActionBar(); if (intent.getBooleanExtra(ALLOW_HOME_UP_KEY, false)) { - ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayHomeAsUpEnabled(true); - - // Set the title to "update location" if we haven't come here - // from the tutorial. - supportActionBar.setTitle(R.string.menu_location); } allowsHomeUp = true; } + if (!allowsHomeUp) { + // From the tutorial. Allow skip. + binding.skipLocationSection.setVisibility(View.VISIBLE); + } + if (AccountManager.Instance.hasLocation(this)) { + // Set the title to "update location" if we have some location already. + if (supportActionBar != null) { + supportActionBar.setTitle(R.string.menu_location); + } + } boolean isDistrictSplit = intent.getBooleanExtra(KEY_IS_DISTRICT_SPLIT, false); if (isDistrictSplit) { binding.districtSplitSuggestion.setVisibility(View.VISIBLE); @@ -110,32 +116,21 @@ protected void onCreate(Bundle savedInstanceState) { } // Set listeners - binding.addressEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - onSubmitAddress(binding.addressEdit.getText().toString()); - return true; - } - return false; - } - }); - - binding.addressSubmit.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + binding.addressEdit.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { onSubmitAddress(binding.addressEdit.getText().toString()); + return true; } + return false; }); + binding.addressSubmit.setOnClickListener(v -> onSubmitAddress(binding.addressEdit.getText().toString())); + + binding.skipLocationBtn.setOnClickListener(v -> returnToMain()); + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS)) { binding.gpsButton.setVisibility(View.VISIBLE); - binding.gpsButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryGettingLocation(); - } - }); + binding.gpsButton.setOnClickListener(v -> tryGettingLocation()); } else { // No GPS available, so don't show the GPS location section. binding.gpsButton.setVisibility(View.GONE); @@ -158,14 +153,15 @@ protected void onPause() { super.onPause(); } + // Returns to wherever we came from or goes back to the MainActivity if we didn't. private void returnToMain() { // Make sure we're still alive if (isFinishing() || isDestroyed()) { return; } - // If we came from MainActivity and return with another Intent, it will create a deep stack - // of activities! + // If we came from MainActivity or IssueActivity and return with another Intent, it will + // create a deep stack of activities! if (allowsHomeUp) { finish(); } else { diff --git a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/MainActivity.java b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/MainActivity.java index f965e084..3e4a9ba5 100644 --- a/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/MainActivity.java +++ b/5calls/app/src/main/java/org/a5calls/android/a5calls/controller/MainActivity.java @@ -85,9 +85,6 @@ public class MainActivity extends AppCompatActivity implements IssuesAdapter.Cal private FiveCallsApi.ContactsRequestListener mContactsRequestListener; private FiveCallsApi.CallRequestListener mReportListener; private OnBackPressedCallback mOnBackPressedCallback; - private String mAddress; - private String mLatitude; - private String mLongitude; private String mLocationName; private String mDistrictId; // If the house district is split for this location. @@ -123,15 +120,6 @@ protected void onCreate(Bundle savedInstanceState) { return; } - // Confirm the user has set a location. - if (!accountManager.hasLocation(this)) { - // No location set, go to LocationActivity! - Intent intent = new Intent(this, LocationActivity.class); - startActivity(intent); - finish(); - return; - } - Intent intent = getIntent(); if (intent != null && intent.getExtras() != null && intent.getExtras().getBoolean(EXTRA_FROM_NOTIFICATION, false)) { @@ -165,53 +153,59 @@ protected void onCreate(Bundle savedInstanceState) { R.dimen.activity_horizontal_margin); final int bottomPadding = getResources().getDimensionPixelSize( R.dimen.activity_vertical_margin); - binding.newsletterSignupView.setPadding(insets.left + sidePadding, 0, + binding.actionableCardHolder.setPadding(insets.left + sidePadding, 0, insets.right + sidePadding, bottomPadding); return WindowInsetsCompat.CONSUMED; }); setupDrawerContent(binding.navigationView); - if (!accountManager.isNewsletterPromptDone(this)) { + boolean hasLocation = accountManager.hasLocation(this); + if (!hasLocation) { + // Show a prompt to set location. + binding.setLocationPrompt.setVisibility(VISIBLE); + binding.setLocationView.setLocationButton.setOnClickListener(view -> { + launchLocationActivity(); + }); + binding.setLocationView.setLocationClickableArea.setOnClickListener(view -> { + launchLocationActivity(); + }); + } else if (!accountManager.isNewsletterPromptDone(this)) { + // Show the newsletter prompt if we have a location and the user hasn't yet + // interacted with the prompt. binding.newsletterSignupView.setVisibility(View.VISIBLE); - binding.newsletterView.newsletterDeclineButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - accountManager.setNewsletterPromptDone(v.getContext(), true); - findViewById(R.id.newsletter_card).setVisibility(View.GONE); - findViewById(R.id.newsletter_card_result_decline).setVisibility(VISIBLE); - } + binding.newsletterView.newsletterDeclineButton.setOnClickListener(v -> { + accountManager.setNewsletterPromptDone(v.getContext(), true); + findViewById(R.id.newsletter_card).setVisibility(View.GONE); + findViewById(R.id.newsletter_card_result_decline).setVisibility(VISIBLE); }); - binding.newsletterView.newsletterSignupButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - String email = binding.newsletterView.newsletterEmail.getText().toString(); - if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { - binding.newsletterView.newsletterEmail.setError( - getResources().getString(R.string.error_email_format)); - return; + binding.newsletterView.newsletterSignupButton.setOnClickListener(v -> { + String email = binding.newsletterView.newsletterEmail.getText().toString(); + if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + binding.newsletterView.newsletterEmail.setError( + getResources().getString(R.string.error_email_format)); + return; + } + binding.newsletterView.newsletterSignupButton.setEnabled(false); + binding.newsletterView.newsletterDeclineButton.setEnabled(false); + FiveCallsApi api = + AppSingleton.getInstance(getApplicationContext()).getJsonController(); + api.newsletterSubscribe(email, new FiveCallsApi.NewsletterSubscribeCallback() { + @Override + public void onSuccess() { + accountManager.setNewsletterPromptDone(v.getContext(), true); + accountManager.setNewsletterSignUpCompleted(v.getContext(), true); + findViewById(R.id.newsletter_card).setVisibility(View.GONE); + findViewById(R.id.newsletter_card_result_success).setVisibility(VISIBLE); } - binding.newsletterView.newsletterSignupButton.setEnabled(false); - binding.newsletterView.newsletterDeclineButton.setEnabled(false); - FiveCallsApi api = - AppSingleton.getInstance(getApplicationContext()).getJsonController(); - api.newsletterSubscribe(email, new FiveCallsApi.NewsletterSubscribeCallback() { - @Override - public void onSuccess() { - accountManager.setNewsletterPromptDone(v.getContext(), true); - accountManager.setNewsletterSignUpCompleted(v.getContext(), true); - findViewById(R.id.newsletter_card).setVisibility(View.GONE); - findViewById(R.id.newsletter_card_result_success).setVisibility(VISIBLE); - } - @Override - public void onError() { - binding.newsletterView.newsletterSignupButton.setEnabled(true); - binding.newsletterView.newsletterDeclineButton.setEnabled(true); - showSnackbar(R.string.newsletter_signup_error, Snackbar.LENGTH_LONG); - } - }); - } + @Override + public void onError() { + binding.newsletterView.newsletterSignupButton.setEnabled(true); + binding.newsletterView.newsletterDeclineButton.setEnabled(true); + showSnackbar(R.string.newsletter_signup_error, Snackbar.LENGTH_LONG); + } + }); }); } @@ -319,11 +313,11 @@ public void onGlobalLayout() { loadStats(); - mAddress = accountManager.getAddress(this); - mLatitude = accountManager.getLat(this); - mLongitude = accountManager.getLng(this); - - if (accountManager.isNewsletterPromptDone(this) || + boolean hasLocation = accountManager.hasLocation(this); + binding.setLocationPrompt.setVisibility(hasLocation ? View.GONE : View.VISIBLE); + if (!hasLocation) { + binding.newsletterSignupView.setVisibility(View.GONE); + } else if (accountManager.isNewsletterPromptDone(this) || accountManager.isNewsletterSignUpCompleted(this)) { binding.newsletterSignupView.setVisibility(View.GONE); } @@ -387,7 +381,7 @@ public void launchSearchDialog() { public void startIssueActivity(Context context, Issue issue) { Intent issueIntent = new Intent(context, IssueActivity.class); issueIntent.putExtra(IssueActivity.KEY_ISSUE, issue); - issueIntent.putExtra(RepCallActivity.KEY_ADDRESS, getLocationString()); + issueIntent.putExtra(RepCallActivity.KEY_ADDRESS, getLocationString(getApplicationContext())); issueIntent.putExtra(RepCallActivity.KEY_LOCATION_NAME, mLocationName); issueIntent.putExtra(IssueActivity.KEY_IS_DISTRICT_SPLIT, mIsDistrictSplit); issueIntent.putExtra(IssueActivity.KEY_IS_LOW_ACCURACY, mIsLowAccuracy); @@ -470,8 +464,9 @@ public void onIssuesReceived(List issues) { mIssuesAdapter.setAllIssues(issues, IssuesAdapter.NO_ERROR); mIssuesAdapter.setFilterAndSearch(mFilterText, mSearchText); binding.swipeContainer.setRefreshing(false); - // Only handle deep link if we have both issues and contacts loaded - if (mIssuesAdapter.hasContacts()) { + // Only handle deep link if we have both issues and contacts loaded, + // or if there's no location set. + if (mIssuesAdapter.hasContacts() || !accountManager.hasLocation(getApplicationContext())) { maybeHandlePendingDeepLink(); } } @@ -485,6 +480,7 @@ public void onRequestError() { // active issues list to avoid showing a stale list. mIssuesAdapter.setAddressError(IssuesAdapter.ERROR_REQUEST); binding.swipeContainer.setRefreshing(false); + maybeHandlePendingDeepLink(); } @Override @@ -494,6 +490,7 @@ public void onJsonError() { // active issues list to avoid showing a stale list. mIssuesAdapter.setAddressError(IssuesAdapter.ERROR_REQUEST); binding.swipeContainer.setRefreshing(false); + maybeHandlePendingDeepLink(); } @Override @@ -501,6 +498,7 @@ public void onAddressError() { showAddressErrorSnackbar(); mIssuesAdapter.setAddressError(IssuesAdapter.ERROR_ADDRESS); binding.swipeContainer.setRefreshing(false); + maybeHandlePendingDeepLink(); } @Override @@ -513,6 +511,7 @@ public void onContactsReceived(String locationName, String districtId, mIsLowAccuracy = isLowAccuracy; binding.collapsingToolbar.setTitle(String.format(getResources().getString( R.string.title_main), mLocationName)); + binding.setLocationPrompt.setVisibility(View.GONE); mIssuesAdapter.setContacts(contacts, mIsDistrictSplit, IssuesAdapter.NO_ERROR); @@ -669,20 +668,25 @@ public void refreshIssues() { FiveCallsApi api = AppSingleton.getInstance(getApplicationContext()).getJsonController(); if (!mIssuesAdapter.hasContacts()) { - String location = getLocationString(); + String location = getLocationString(getApplicationContext()); if (!TextUtils.isEmpty(location)) { api.getContacts(location); + } else { + mIssuesAdapter.setAddressError(IssuesAdapter.ERROR_ADDRESS); } } api.getIssues(); } - private String getLocationString() { - if (!TextUtils.isEmpty(mLatitude) && !TextUtils.isEmpty(mLongitude)) { - return mLatitude + "," + mLongitude; + public static String getLocationString(Context context) { + String address = AccountManager.Instance.getAddress(context); + String latitude = AccountManager.Instance.getLat(context); + String longitude = AccountManager.Instance.getLng(context); + if (!TextUtils.isEmpty(latitude) && !TextUtils.isEmpty(longitude)) { + return latitude + "," + longitude; - } else if (!TextUtils.isEmpty(mAddress)) { - return mAddress; + } else if (!TextUtils.isEmpty(address)) { + return address; } return null; } @@ -803,8 +807,13 @@ private void maybeHandlePendingDeepLink() { return; } - // Wait for both issues and contacts to be loaded before handling deep link - if (mIssuesAdapter.getAllIssues().isEmpty() || !mIssuesAdapter.hasContacts()) { + // Wait for both issues and contacts to be loaded before handling deep link, + // or just issues if we have no location. + boolean hasLocation = accountManager.hasLocation(getApplicationContext()); + if (mIssuesAdapter.getAllIssues().isEmpty()) { + return; + } + if (!mIssuesAdapter.hasContacts() && hasLocation && !mIssuesAdapter.hasAddressError()) { return; } @@ -824,9 +833,11 @@ private void maybeHandlePendingDeepLink() { } if (targetIssue != null) { - // Populate the issue's contacts before launching IssueActivity - // This is normally done in IssuesAdapter.onBindViewHolder, but we're bypassing that - mIssuesAdapter.populateIssueContacts(targetIssue); + if (hasLocation && !mIssuesAdapter.hasAddressError()) { + // Populate the issue's contacts before launching IssueActivity + // This is normally done in IssuesAdapter.onBindViewHolder, but we're bypassing that + mIssuesAdapter.populateIssueContacts(targetIssue); + } startIssueActivity(this, targetIssue); } else { hideSnackbars(); 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 c1076e0f..98731a0a 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 @@ -260,7 +260,8 @@ public void onCallReported() { // Return to the main activity AccountManager.Instance.setTutorialSeen(getActivity(), true); - Intent intent = new Intent(getActivity(), MainActivity.class); + Intent intent = new Intent(getActivity(), LocationActivity.class); + intent.putExtra(LocationActivity.ALLOW_HOME_UP_KEY, false); startActivity(intent); getActivity().finish(); }); diff --git a/5calls/app/src/main/res/layout/activity_issue.xml b/5calls/app/src/main/res/layout/activity_issue.xml index f323c244..08479d6e 100644 --- a/5calls/app/src/main/res/layout/activity_issue.xml +++ b/5calls/app/src/main/res/layout/activity_issue.xml @@ -121,7 +121,7 @@ @@ -145,6 +145,42 @@ + + + + + + +