diff --git a/app/build.gradle b/app/build.gradle
index 66958b6b..d0f67b34 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -68,7 +68,7 @@ dependencies {
implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:cardview-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion"
- implementation 'com.android.support.constraint:constraint-layout:1.1.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.1'
// Android Play services libraries
def playServicesVersion = '15.0.1'
implementation "com.google.android.gms:play-services-location:$playServicesVersion"
diff --git a/app/schemas/com.gophillygo.app.data.GpgDatabase/11.json b/app/schemas/com.gophillygo.app.data.GpgDatabase/11.json
new file mode 100644
index 00000000..a1bcc39e
--- /dev/null
+++ b/app/schemas/com.gophillygo.app.data.GpgDatabase/11.json
@@ -0,0 +1,530 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 11,
+ "identityHash": "5cbe98c663ba7f4df4514cb325906a9a",
+ "entities": [
+ {
+ "tableName": "AttractionFlag",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`attraction_id` INTEGER NOT NULL, `is_event` INTEGER NOT NULL, `option` INTEGER, PRIMARY KEY(`attraction_id`, `is_event`))",
+ "fields": [
+ {
+ "fieldPath": "attractionID",
+ "columnName": "attraction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isEvent",
+ "columnName": "is_event",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "option",
+ "columnName": "option",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "attraction_id",
+ "is_event"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AttractionFlag_is_event_option",
+ "unique": false,
+ "columnNames": [
+ "is_event",
+ "option"
+ ],
+ "createSql": "CREATE INDEX `index_AttractionFlag_is_event_option` ON `${TABLE_NAME}` (`is_event`, `option`)"
+ },
+ {
+ "name": "index_AttractionFlag_attraction_id",
+ "unique": false,
+ "columnNames": [
+ "attraction_id"
+ ],
+ "createSql": "CREATE INDEX `index_AttractionFlag_attraction_id` ON `${TABLE_NAME}` (`attraction_id`)"
+ },
+ {
+ "name": "index_AttractionFlag_is_event",
+ "unique": false,
+ "columnNames": [
+ "is_event"
+ ],
+ "createSql": "CREATE INDEX `index_AttractionFlag_is_event` ON `${TABLE_NAME}` (`is_event`)"
+ },
+ {
+ "name": "index_AttractionFlag_option",
+ "unique": false,
+ "columnNames": [
+ "option"
+ ],
+ "createSql": "CREATE INDEX `index_AttractionFlag_option` ON `${TABLE_NAME}` (`option`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Destination",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`city` TEXT, `state` TEXT, `address` TEXT, `categories` TEXT, `watershed_alliance` INTEGER NOT NULL, `zipcode` TEXT, `distance` REAL NOT NULL, `id` INTEGER NOT NULL, `placeID` INTEGER NOT NULL, `name` TEXT, `accessible` INTEGER NOT NULL, `image` TEXT, `cycling` INTEGER NOT NULL, `description` TEXT, `priority` INTEGER NOT NULL, `activities` TEXT, `website_url` TEXT, `wide_image` TEXT, `is_event` INTEGER NOT NULL, `extra_wide_images` TEXT, `timestamp` INTEGER NOT NULL, `nature` INTEGER, `exercise` INTEGER, `educational` INTEGER, `x` REAL, `y` REAL, `street_address` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "city",
+ "columnName": "city",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "categories",
+ "columnName": "categories",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "watershedAlliance",
+ "columnName": "watershed_alliance",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "zipCode",
+ "columnName": "zipcode",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "distance",
+ "columnName": "distance",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "placeID",
+ "columnName": "placeID",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accessible",
+ "columnName": "accessible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cycling",
+ "columnName": "cycling",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activities",
+ "columnName": "activities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "websiteUrl",
+ "columnName": "website_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "wideImage",
+ "columnName": "wide_image",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEvent",
+ "columnName": "is_event",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "extraWideImages",
+ "columnName": "extra_wide_images",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "categoryFlags.nature",
+ "columnName": "nature",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "categoryFlags.exercise",
+ "columnName": "exercise",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "categoryFlags.educational",
+ "columnName": "educational",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "location.x",
+ "columnName": "x",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "location.y",
+ "columnName": "y",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attributes.streetAddress",
+ "columnName": "street_address",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Destination_educational",
+ "unique": false,
+ "columnNames": [
+ "educational"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_educational` ON `${TABLE_NAME}` (`educational`)"
+ },
+ {
+ "name": "index_Destination_nature",
+ "unique": false,
+ "columnNames": [
+ "nature"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_nature` ON `${TABLE_NAME}` (`nature`)"
+ },
+ {
+ "name": "index_Destination_exercise",
+ "unique": false,
+ "columnNames": [
+ "exercise"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_exercise` ON `${TABLE_NAME}` (`exercise`)"
+ },
+ {
+ "name": "index_Destination_watershed_alliance",
+ "unique": false,
+ "columnNames": [
+ "watershed_alliance"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_watershed_alliance` ON `${TABLE_NAME}` (`watershed_alliance`)"
+ },
+ {
+ "name": "index_Destination_distance",
+ "unique": false,
+ "columnNames": [
+ "distance"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_distance` ON `${TABLE_NAME}` (`distance`)"
+ },
+ {
+ "name": "index_Destination_placeID",
+ "unique": false,
+ "columnNames": [
+ "placeID"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_placeID` ON `${TABLE_NAME}` (`placeID`)"
+ },
+ {
+ "name": "index_Destination_name",
+ "unique": false,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_name` ON `${TABLE_NAME}` (`name`)"
+ },
+ {
+ "name": "index_Destination_accessible",
+ "unique": false,
+ "columnNames": [
+ "accessible"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_accessible` ON `${TABLE_NAME}` (`accessible`)"
+ },
+ {
+ "name": "index_Destination_cycling",
+ "unique": false,
+ "columnNames": [
+ "cycling"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_cycling` ON `${TABLE_NAME}` (`cycling`)"
+ },
+ {
+ "name": "index_Destination_activities",
+ "unique": false,
+ "columnNames": [
+ "activities"
+ ],
+ "createSql": "CREATE INDEX `index_Destination_activities` ON `${TABLE_NAME}` (`activities`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Event",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`destination` INTEGER, `start_date` TEXT, `end_date` TEXT, `id` INTEGER NOT NULL, `placeID` INTEGER NOT NULL, `name` TEXT, `accessible` INTEGER NOT NULL, `image` TEXT, `cycling` INTEGER NOT NULL, `description` TEXT, `priority` INTEGER NOT NULL, `activities` TEXT, `website_url` TEXT, `wide_image` TEXT, `is_event` INTEGER NOT NULL, `extra_wide_images` TEXT, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`destination`) REFERENCES `Destination`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED)",
+ "fields": [
+ {
+ "fieldPath": "destination",
+ "columnName": "destination",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "startDate",
+ "columnName": "start_date",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "endDate",
+ "columnName": "end_date",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "placeID",
+ "columnName": "placeID",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accessible",
+ "columnName": "accessible",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cycling",
+ "columnName": "cycling",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "priority",
+ "columnName": "priority",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activities",
+ "columnName": "activities",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "websiteUrl",
+ "columnName": "website_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "wideImage",
+ "columnName": "wide_image",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isEvent",
+ "columnName": "is_event",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "extraWideImages",
+ "columnName": "extra_wide_images",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Event_destination",
+ "unique": false,
+ "columnNames": [
+ "destination"
+ ],
+ "createSql": "CREATE INDEX `index_Event_destination` ON `${TABLE_NAME}` (`destination`)"
+ },
+ {
+ "name": "index_Event_start_date",
+ "unique": false,
+ "columnNames": [
+ "start_date"
+ ],
+ "createSql": "CREATE INDEX `index_Event_start_date` ON `${TABLE_NAME}` (`start_date`)"
+ },
+ {
+ "name": "index_Event_end_date",
+ "unique": false,
+ "columnNames": [
+ "end_date"
+ ],
+ "createSql": "CREATE INDEX `index_Event_end_date` ON `${TABLE_NAME}` (`end_date`)"
+ },
+ {
+ "name": "index_Event_placeID",
+ "unique": false,
+ "columnNames": [
+ "placeID"
+ ],
+ "createSql": "CREATE INDEX `index_Event_placeID` ON `${TABLE_NAME}` (`placeID`)"
+ },
+ {
+ "name": "index_Event_name",
+ "unique": false,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE INDEX `index_Event_name` ON `${TABLE_NAME}` (`name`)"
+ },
+ {
+ "name": "index_Event_accessible",
+ "unique": false,
+ "columnNames": [
+ "accessible"
+ ],
+ "createSql": "CREATE INDEX `index_Event_accessible` ON `${TABLE_NAME}` (`accessible`)"
+ },
+ {
+ "name": "index_Event_cycling",
+ "unique": false,
+ "columnNames": [
+ "cycling"
+ ],
+ "createSql": "CREATE INDEX `index_Event_cycling` ON `${TABLE_NAME}` (`cycling`)"
+ },
+ {
+ "name": "index_Event_activities",
+ "unique": false,
+ "columnNames": [
+ "activities"
+ ],
+ "createSql": "CREATE INDEX `index_Event_activities` ON `${TABLE_NAME}` (`activities`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "Destination",
+ "onDelete": "SET NULL",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "destination"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"5cbe98c663ba7f4df4514cb325906a9a\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31933e04..7ae2e213 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -101,8 +101,6 @@
-
diff --git a/app/src/main/java/com/gophillygo/app/FilterDialog.java b/app/src/main/java/com/gophillygo/app/FilterDialog.java
index f1797580..a1eaae20 100644
--- a/app/src/main/java/com/gophillygo/app/FilterDialog.java
+++ b/app/src/main/java/com/gophillygo/app/FilterDialog.java
@@ -20,7 +20,7 @@ public class FilterDialog extends BottomSheetDialogFragment {
private FilterModalBinding binding;
public interface FilterChangeListener {
- void filterChanged(Filter filter);
+ void setFilter(Filter filter);
}
private static final String LOG_LABEL = "FilterDialog";
@@ -41,7 +41,7 @@ public void onDismiss(DialogInterface dialog) {
Log.d(LOG_LABEL, "Selected " + String.valueOf(filter.count()) + " filters.");
FilterChangeListener listener = (FilterChangeListener) getActivity();
if (listener != null) {
- listener.filterChanged(filter);
+ listener.setFilter(filter);
}
super.onDismiss(dialog);
diff --git a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java
index ce25c8f2..3c14207e 100644
--- a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java
+++ b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java
@@ -116,6 +116,7 @@ protected FilterButtonBarBinding setupDataBinding() {
@Override
protected void loadData() {
+ if (eventsListView == null) return;
List filteredEvents = getFilteredEvents();
TextView noDataView = findViewById(R.id.empty_events_list);
@@ -162,6 +163,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
@NonNull
private List getFilteredEvents() {
+ if (events == null) return new ArrayList<>(0);
List filteredEvents = new ArrayList<>(events.size());
for (EventInfo info : events) {
if (filter.matches(info)) {
diff --git a/app/src/main/java/com/gophillygo/app/activities/FilterableListActivity.java b/app/src/main/java/com/gophillygo/app/activities/FilterableListActivity.java
index e3f958b3..e5435fd9 100644
--- a/app/src/main/java/com/gophillygo/app/activities/FilterableListActivity.java
+++ b/app/src/main/java/com/gophillygo/app/activities/FilterableListActivity.java
@@ -22,11 +22,12 @@
public abstract class FilterableListActivity extends BaseAttractionActivity
implements FilterDialog.FilterChangeListener, ToolbarFilterListener {
+ public final static String FILTER_KEY = "filter";
+
private final int toolbarId;
private Button filterButton;
private Drawable filterIcon;
- private Toolbar toolbar;
protected Filter filter;
private FilterButtonBarBinding filterBinding;
@@ -44,10 +45,11 @@ protected void onCreate(Bundle savedInstanceState) {
filterBinding = setupDataBinding();
filterBinding.setListener(this);
+ filter = new Filter();
filterBinding.setFilter(filter);
// set up toolbar
- toolbar = findViewById(toolbarId);
+ Toolbar toolbar = findViewById(toolbarId);
setSupportActionBar(toolbar);
//noinspection ConstantConditions
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -55,7 +57,6 @@ protected void onCreate(Bundle savedInstanceState) {
filterIcon = ContextCompat.getDrawable(this, R.drawable.ic_filter_list_white_24dp);
// set up filter button
- filter = new Filter();
filterButton = findViewById(R.id.filter_bar_filter_button);
filterButton.setOnClickListener(v -> {
// Need to give the filter dialog a copy of the filter, or toggling the
@@ -63,10 +64,22 @@ protected void onCreate(Bundle savedInstanceState) {
FilterDialog filterDialog = FilterDialog.newInstance(new Filter(filter));
filterDialog.show(getSupportFragmentManager(), filterDialog.getTag());
});
+
+ if (getIntent().hasExtra(FILTER_KEY)) {
+ setFilter((Filter) getIntent().getSerializableExtra(FILTER_KEY));
+ } else if (savedInstanceState != null && savedInstanceState.containsKey(FILTER_KEY)) {
+ setFilter((Filter) savedInstanceState.getSerializable(FILTER_KEY));
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putSerializable(FILTER_KEY, filter);
+ super.onSaveInstanceState(outState);
}
@Override
- public void filterChanged(Filter filter) {
+ public void setFilter(Filter filter) {
this.filter = filter;
filterBinding.setFilter(filter);
filterBinding.notifyPropertyChanged(BR.filter);
@@ -91,13 +104,13 @@ public void filterChanged(Filter filter) {
@Override
public void toggleLiked() {
filter.setLiked(filterBinding.filterBarLikedButton.isChecked());
- filterChanged(filter);
+ setFilter(filter);
}
@Override
public void toggleWantToGo() {
filter.setWantToGo(filterBinding.filterBarWantToGoButton.isChecked());
- filterChanged(filter);
+ setFilter(filter);
}
}
diff --git a/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java b/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java
index c41373ad..8a0a79ea 100644
--- a/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java
+++ b/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java
@@ -1,42 +1,61 @@
package com.gophillygo.app.activities;
+import android.arch.lifecycle.LiveData;
import android.content.Intent;
import android.os.Bundle;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
-import android.widget.GridView;
import com.gophillygo.app.CarouselViewListener;
import com.gophillygo.app.R;
import com.gophillygo.app.adapters.PlaceCategoryGridAdapter;
+import com.gophillygo.app.data.DestinationRepository;
+import com.gophillygo.app.data.models.CategoryAttraction;
import com.gophillygo.app.data.models.Destination;
+import com.gophillygo.app.data.models.Filter;
import com.synnapps.carouselview.CarouselView;
+import java.util.ArrayList;
+import java.util.List;
-public class HomeActivity extends BaseAttractionActivity {
+import static com.gophillygo.app.activities.FilterableListActivity.FILTER_KEY;
+
+
+public class HomeActivity extends BaseAttractionActivity implements DestinationRepository.CategoryAttractionCallback,
+PlaceCategoryGridAdapter.GridViewHolder.PlaceGridItemClickListener {
private static final String LOG_LABEL = "HomeActivity";
- private CarouselView carouselView;
+ // how many columns to display in grid of filter buttons
+ private static final int NUM_COLUMNS = 2;
+ private CarouselView carouselView;
+ RecyclerView recyclerView;
+ PlaceCategoryGridAdapter gridAdapter;
+ List categories;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
Log.d(LOG_LABEL, "onCreate");
+ categories = new ArrayList<>(CategoryAttraction.PlaceCategories.size());
Toolbar toolbar = findViewById(R.id.home_toolbar);
// disable default app name title display
toolbar.setTitle("");
setSupportActionBar(toolbar);
- GridView gridView = findViewById(R.id.home_grid_view);
- gridView.setAdapter(new PlaceCategoryGridAdapter(this));
- gridView.setOnItemClickListener((parent, v, position, id) -> clickedGridItem(position));
+ recyclerView = findViewById(R.id.home_grid_view);
+ GridLayoutManager layoutManager = new GridLayoutManager(this, NUM_COLUMNS);
+ recyclerView.setLayoutManager(layoutManager);
+ gridAdapter = new PlaceCategoryGridAdapter(this, this);
+ recyclerView.setAdapter(gridAdapter);
carouselView = findViewById(R.id.home_carousel);
carouselView.setIndicatorGravity(Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM);
@@ -45,7 +64,6 @@ protected void onCreate(Bundle savedInstanceState) {
// initialize carousel if destinations already loaded
locationOrDestinationsChanged();
-
}
@Override
@@ -54,6 +72,8 @@ public void locationOrDestinationsChanged() {
// set up carousel with nearest destinations
if (getNearestDestinationSize() > 0) {
setUpCarousel();
+ // request random images for the filter grid categories, and notify the adapter
+ viewModel.getCategoryAttractions(this);
} else {
Log.w(LOG_LABEL, "No nearest destinations yet in locationOrDestinationChanged");
}
@@ -123,7 +143,8 @@ protected void onPostResume() {
carouselView.playCarousel();
}
- private void clickedGridItem(int position) {
+ @Override
+ public void clickedGridItem(int position) {
Log.d(LOG_LABEL, "clicked grid view item: " + position);
switch (position) {
@@ -133,8 +154,70 @@ private void clickedGridItem(int position) {
break;
default:
// go to places list
- // TODO: #18 filter list based on selected grid item
- startActivity(new Intent(this, PlacesListActivity.class));
+ if (categories == null || position >= categories.size()) {
+ Log.e(LOG_LABEL, "Cannot go to filtered list because categories are missing");
+ startActivity(new Intent(this, PlacesListActivity.class));
+ }
+ CategoryAttraction attraction = categories.get(position);
+ goToFilteredPlacesList(attraction.getCategory());
}
}
+
+ private void goToFilteredPlacesList(CategoryAttraction.PlaceCategories category) {
+ Filter filter = new Filter();
+ switch (category) {
+ case WantToGo:
+ filter.setWantToGo(true);
+ break;
+ case Liked:
+ filter.setLiked(true);
+ break;
+ case Nature:
+ filter.setNature(true);
+ break;
+ case Exercise:
+ filter.setExercise(true);
+ break;
+ case Educational:
+ filter.setEducational(true);
+ break;
+ default:
+ Log.e(LOG_LABEL, "Unrecognized place category " + category.displayName);
+ }
+ Intent intent = new Intent(this, PlacesListActivity.class);
+ intent.putExtra(FILTER_KEY, filter);
+ startActivity(intent);
+ }
+
+ @Override
+ public void gotCategoryAttractions(LiveData> categoryAttractions) {
+ Log.d(LOG_LABEL, "Getting category attractions");
+ categoryAttractions.observe(this, data -> {
+ recyclerView.post(() -> {
+ Log.d(LOG_LABEL, "Got category attractions");
+ if (data == null || data.isEmpty()) {
+ Log.e(LOG_LABEL, "Category attractions are missing");
+ return;
+ }
+
+ // Submit `categories` list managed by this activity, rather than the `data` list
+ // owned by the DAO, in order to be able to remove categories with no entries
+ // (empty image). Do not destroy/recreate list here, so list differ will
+ // correctly recognize that the reference hasn't changed.
+
+ categories.clear();
+ categories.addAll(data);
+ gridAdapter.submitList(categories);
+ gridAdapter.notifyDataSetChanged();
+
+ for (CategoryAttraction attraction: data) {
+ if (attraction.getImage().isEmpty()) {
+ categories.remove(attraction);
+ gridAdapter.submitList(categories);
+ gridAdapter.notifyItemRemoved(attraction.getCategory().code);
+ }
+ }
+ });
+ });
+ }
}
diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java
index d4f9e6c0..334e301e 100644
--- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java
+++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java
@@ -32,7 +32,6 @@ public class PlaceDetailActivity extends AttractionDetailActivity {
private ActivityPlaceDetailBinding binding;
private CarouselView carouselView;
- private Toolbar toolbar;
@SuppressWarnings("WeakerAccess")
@Inject
@@ -46,7 +45,7 @@ protected void onCreate(Bundle savedInstanceState) {
binding = DataBindingUtil.setContentView(this, R.layout.activity_place_detail);
binding.setActivity(this);
- toolbar = findViewById(R.id.place_detail_toolbar);
+ Toolbar toolbar = findViewById(R.id.place_detail_toolbar);
// disable default app name title display
toolbar.setTitle("");
setSupportActionBar(toolbar);
@@ -101,7 +100,6 @@ private void displayDestination() {
.getQuantityString(R.plurals.place_upcoming_activities_count, eventCount, eventCount);
upcomingEventsView.setText(upcomingEventsText);
- // TODO: #18 go to filtered event list with events for destination on click
upcomingEventsView.setOnClickListener(v -> Log.d(LOG_LABEL,
"Clicked upcoming events for destination " + destinationInfo.getDestination().getName()));
diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java
index d7f85c0d..56c27b46 100644
--- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java
+++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java
@@ -16,12 +16,9 @@
import com.gophillygo.app.adapters.PlacesListAdapter;
import com.gophillygo.app.data.models.AttractionFlag;
import com.gophillygo.app.data.models.AttractionInfo;
-import com.gophillygo.app.data.models.Destination;
import com.gophillygo.app.data.models.DestinationInfo;
import com.gophillygo.app.databinding.ActivityPlacesListBinding;
import com.gophillygo.app.databinding.FilterButtonBarBinding;
-import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver;
-import com.gophillygo.app.tasks.RemoveGeofenceWorker;
import java.util.ArrayList;
import java.util.List;
@@ -31,19 +28,11 @@ public class PlacesListActivity extends FilterableListActivity implements
private static final String LOG_LABEL = "PlacesList";
- private LinearLayoutManager layoutManager;
private RecyclerView placesListView;
PlacesListAdapter placesListAdapter;
public PlacesListActivity() {
super(R.id.places_list_toolbar);
-
- // If destinations were loaded before this activity showed, use them immediately.
- if (destinationInfos != null && !destinationInfos.isEmpty()) {
- loadData();
- } else {
- Log.d(LOG_LABEL, "Have no destinations for the places list");
- }
}
@Override
@@ -52,7 +41,7 @@ public void locationOrDestinationsChanged() {
if (destinationInfos != null && !destinationInfos.isEmpty()) {
loadData();
} else {
- Log.d(LOG_LABEL, "Have no destinations for the places list in locationOrDestinationsChanged");
+ Log.w(LOG_LABEL, "Have no destinations for the places list in locationOrDestinationsChanged");
}
}
@@ -86,8 +75,16 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set up list of places
- layoutManager = new LinearLayoutManager(this);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
placesListView = findViewById(R.id.places_list_recycler_view);
+ placesListView.setLayoutManager(layoutManager);
+
+ // If destinations were loaded before this activity showed, use them immediately.
+ if (destinationInfos != null && !destinationInfos.isEmpty()) {
+ loadData();
+ } else {
+ Log.d(LOG_LABEL, "Have no destinations for the places list");
+ }
}
@Override
@@ -98,6 +95,7 @@ protected FilterButtonBarBinding setupDataBinding() {
@Override
protected void loadData() {
+ if (placesListView == null) return;
Log.d(LOG_LABEL, "loadData");
List filteredDestinations = getFilteredDestinations();
@@ -110,13 +108,14 @@ protected void loadData() {
// must set the list before the adapter for the differ to initialize properly
placesListAdapter.submitList(filteredDestinations);
placesListView.setAdapter(placesListAdapter);
- placesListView.setLayoutManager(layoutManager);
} else {
Log.d(LOG_LABEL, "submit list for diff");
// Let the AsyncListDiffer find which have changed, and only update their view holders
// https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter
placesListAdapter.submitList(filteredDestinations);
}
+ placesListAdapter.notifyDataSetChanged();
+ placesListView.requestLayout();
}
@Override
@@ -148,6 +147,9 @@ public boolean onOptionsItemSelected(MenuItem item) {
@NonNull
private ArrayList getFilteredDestinations() {
+ if (destinationInfos == null) {
+ return new ArrayList<>(0);
+ }
ArrayList filteredDestinations = new ArrayList<>(destinationInfos.size());
for (DestinationInfo info : destinationInfos) {
if (filter.matches(info)) {
diff --git a/app/src/main/java/com/gophillygo/app/adapters/AttractionListAdapter.java b/app/src/main/java/com/gophillygo/app/adapters/AttractionListAdapter.java
index 9deb494a..728253f4 100644
--- a/app/src/main/java/com/gophillygo/app/adapters/AttractionListAdapter.java
+++ b/app/src/main/java/com/gophillygo/app/adapters/AttractionListAdapter.java
@@ -102,8 +102,8 @@ public void optionsButtonClick(View view, T info, Integer position) {
@Override
public void submitList(List list) {
- super.submitList(list);
this.attractionList = list;
+ super.submitList(list);
}
@NonNull
diff --git a/app/src/main/java/com/gophillygo/app/adapters/PlaceCategoryGridAdapter.java b/app/src/main/java/com/gophillygo/app/adapters/PlaceCategoryGridAdapter.java
index 3741e5e8..7ed792cb 100644
--- a/app/src/main/java/com/gophillygo/app/adapters/PlaceCategoryGridAdapter.java
+++ b/app/src/main/java/com/gophillygo/app/adapters/PlaceCategoryGridAdapter.java
@@ -1,97 +1,119 @@
package com.gophillygo.app.adapters;
import android.content.Context;
+import android.databinding.DataBindingUtil;
+import android.databinding.ViewDataBinding;
+import android.support.annotation.NonNull;
+import android.support.v7.recyclerview.extensions.ListAdapter;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.GridView;
-import android.widget.ImageView;
-import android.widget.TextView;
-import com.bumptech.glide.Glide;
+import com.gophillygo.app.BR;
import com.gophillygo.app.R;
+import com.gophillygo.app.data.models.CategoryAttraction;
+import java.util.List;
+import java.util.Objects;
-public class PlaceCategoryGridAdapter extends BaseAdapter {
- private final Context context;
- private final LayoutInflater inflater;
+public class PlaceCategoryGridAdapter extends ListAdapter {
- private static class ViewHolder {
- ImageView imageView;
- TextView categoryNameView;
+ private static final String LOG_LABEL = "GridAdapter";
+
+ private List categoryAttractions;
+
+ private LayoutInflater inflater;
+ private GridViewHolder.PlaceGridItemClickListener listener;
+
+ public static class GridViewHolder extends RecyclerView.ViewHolder {
+ private final ViewDataBinding binding;
+
+ public interface PlaceGridItemClickListener {
+ void clickedGridItem(int position);
+ }
+
+ GridViewHolder(ViewDataBinding binding, final PlaceGridItemClickListener listener) {
+ super(binding.getRoot());
+ binding.getRoot().setOnClickListener(v -> listener.clickedGridItem(getAdapterPosition()));
+ this.binding = binding;
+ }
+ public void bind(CategoryAttraction info) {
+ binding.setVariable(BR.category, info);
+ binding.setVariable(BR.position, getAdapterPosition());
+ binding.executePendingBindings();
+ }
+ }
+
+ private PlaceCategoryGridAdapter() {
+ super(new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(CategoryAttraction oldItem, CategoryAttraction newItem) {
+ if (oldItem == null) {
+ return newItem == null;
+ } else {
+ return newItem != null && oldItem.getCategory().equals(newItem.getCategory());
+ }
+ }
+
+ @Override
+ public boolean areContentsTheSame(CategoryAttraction oldItem, CategoryAttraction newItem) {
+ return Objects.equals(oldItem, newItem);
+ }
+ });
}
- private static final int CATEGORIES_COUNT = 6;
-
- private static final String[] placeCategoryNames = {
- "Upcoming events",
- "Want to go",
- "Nature",
- "Places you like",
- "Exercise",
- "Educational"
- };
-
- private static final String[] placeCategoryImages = {
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/4c5f9e802b89495da7e485a4449df220.jpg",
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/60a86b43597a4b8f8e129f9d6435960a.jpg",
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/e6aa6bc0891247c4a4d651f22c028fe6.jpg",
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/874f2bd93b5f4bc692cf39d1aaba5ead.jpg",
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/4c5f9e802b89495da7e485a4449df220.jpg",
- "https://cleanair-images-prod.s3.amazonaws.com/destinations/ad72d3d20dfb4197b76c7b4d211a8eef.jpg"
- };
-
- public PlaceCategoryGridAdapter(Context context) {
- this.context = context;
+ public PlaceCategoryGridAdapter(Context context, GridViewHolder.PlaceGridItemClickListener listener) {
+ this();
this.inflater = LayoutInflater.from(context);
+ this.listener = listener;
}
+ @NonNull
@Override
- public int getCount() {
- return CATEGORIES_COUNT;
+ public GridViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ Log.d(LOG_LABEL, "onCreateViewHolder");
+
+ ViewDataBinding binding = DataBindingUtil.inflate(inflater,
+ R.layout.place_category_grid_item, parent, false);
+ binding.setVariable(BR.adapter, this);
+ return new GridViewHolder(binding, this.listener);
}
@Override
- public Object getItem(int position) {
- return null;
+ public void onBindViewHolder(@NonNull GridViewHolder holder, int position) {
+ CategoryAttraction item = getItem(position);
+ holder.bind(item);
+ holder.itemView.setTag(item);
}
@Override
- public long getItemId(int position) {
- return 0;
+ protected CategoryAttraction getItem(int position) {
+ if (categoryAttractions == null) {
+ return null;
+ }
+ return categoryAttractions.get(position);
}
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- ViewHolder viewHolder;
-
- if (convertView == null) {
- convertView = inflater.inflate(R.layout.place_category_grid_item, parent, false);
- viewHolder = new ViewHolder();
- viewHolder.imageView = convertView.findViewById(R.id.place_category_grid_item_image);
- viewHolder.categoryNameView = convertView.findViewById(R.id.place_category_grid_item_name);
-
- // size the image to fill its square grid box
- ViewGroup.LayoutParams params = viewHolder.imageView.getLayoutParams();
- int columnSize = ((GridView) parent).getColumnWidth();
- params.width = columnSize;
- params.height = columnSize;
-
- convertView.setTag(viewHolder);
- } else {
- viewHolder = (ViewHolder) convertView.getTag();
+ public long getItemId(int position) {
+ CategoryAttraction attraction = getItem(position);
+ if (attraction == null) {
+ return -1;
}
+ return attraction.getCategory().code;
+ }
- Glide.with(context)
- .load(placeCategoryImages[position])
- .into(viewHolder.imageView);
-
- viewHolder.imageView.setContentDescription(placeCategoryNames[position]);
- viewHolder.categoryNameView.setText(placeCategoryNames[position]);
+ @Override
+ public void submitList(List list) {
+ super.submitList(list);
+ this.categoryAttractions = list;
+ }
- return convertView;
+ @Override
+ public int getItemCount() {
+ return categoryAttractions == null ? 0 : categoryAttractions.size();
}
}
diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java
index 007a96ed..3301f970 100644
--- a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java
+++ b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java
@@ -1,14 +1,21 @@
package com.gophillygo.app.data;
import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MediatorLiveData;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
+import android.util.Log;
+import com.gophillygo.app.data.models.Attraction;
import com.gophillygo.app.data.models.AttractionFlag;
+import com.gophillygo.app.data.models.CategoryAttraction;
+import com.gophillygo.app.data.models.CategoryAttraction.PlaceCategories;
+import com.gophillygo.app.data.models.CategoryImage;
import com.gophillygo.app.data.models.Destination;
import com.gophillygo.app.data.models.DestinationInfo;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -17,11 +24,15 @@
@Dao
public abstract class DestinationDao implements AttractionDao {
+
+ private static final String LOG_LABEL = "DestinationDao";
+
+ @Transaction
@Query("SELECT destination.*, COUNT(event.id) AS eventCount, attractionflag.option " +
"FROM destination " +
"LEFT JOIN event ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON destination.id = attractionflag.attractionID AND attractionflag.is_event = 0 " +
+ "ON destination.id = attractionflag.attraction_id AND attractionflag.is_event = 0 " +
"GROUP BY destination.id " +
"ORDER BY distance ASC")
public abstract LiveData> getAll();
@@ -30,7 +41,7 @@ public abstract class DestinationDao implements AttractionDao {
"FROM destination " +
"LEFT JOIN event ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON destination.id = attractionflag.attractionID AND attractionflag.is_event = 0 " +
+ "ON destination.id = attractionflag.attraction_id AND attractionflag.is_event = 0 " +
"WHERE destination.id = :destinationId " +
"GROUP BY destination.id")
abstract LiveData getDestination(long destinationId);
@@ -45,6 +56,98 @@ public void bulkUpdate(List destinations) {
}
}
+ @Query("SELECT id AS attractionID, 1 AS is_event, image FROM event ORDER BY random() LIMIT 1")
+ protected abstract LiveData getEventCategoryImage();
+
+ @Query("SELECT id AS attractionID, 0 AS is_event, destination.image AS image " +
+ "FROM destination " +
+ "LEFT JOIN attractionflag " +
+ "ON destination.id = attractionflag.attraction_id AND attractionflag.is_event = 0 " +
+ "WHERE attractionflag.option = :flag " +
+ "ORDER BY random() " +
+ "LIMIT 1")
+ public abstract LiveData getRandomImagesForFlag(int flag);
+
+ @Query("SELECT id AS attractionID, 0 AS is_event, destination.image AS image " +
+ "FROM destination " +
+ "WHERE nature = 1 " +
+ "ORDER BY random() " +
+ "LIMIT 1")
+ public abstract LiveData getRandomNatureImages();
+
+ @Query("SELECT id AS attractionID, 0 AS is_event, destination.image AS image " +
+ "FROM destination " +
+ "WHERE exercise = 1 " +
+ "ORDER BY random() " +
+ "LIMIT 1")
+ public abstract LiveData getRandomExerciseImages();
+
+ /**
+ * Find random representative images for each of the filter buttons on the home screen.
+ * Must be called from background thread.
+ *
+ * One category is events, fetched here instead of {@link EventDao} for convenience and to
+ * keep requests to a single transaction. The others are based on either destination category
+ * or user flag.
+ *
+ * Gets a randomized set of {@link Attraction} for each filter button. May return duplicates.
+ *
+ * @return One randomized image for each of the home page grid of filter categories
+ */
+ @Transaction
+ public LiveData> getCategoryImages() {
+ ArrayList categoryAttractions = new ArrayList<>(CategoryAttraction.PlaceCategories.size());
+ for (CategoryAttraction.PlaceCategories placeCategory : CategoryAttraction.PlaceCategories.values()) {
+ categoryAttractions.add(placeCategory.code, new CategoryAttraction(placeCategory.code, ""));
+ }
+ Log.d(LOG_LABEL, "created category grid adapter");
+
+ MediatorLiveData> data = new MediatorLiveData<>();
+ LiveData event = getEventCategoryImage();
+ addSourceToRandomImagesLiveData(event, PlaceCategories.Events, categoryAttractions, data);
+
+ LiveData wantToGo = getRandomImagesForFlag(AttractionFlag.Option.WantToGo.code);
+ addSourceToRandomImagesLiveData(wantToGo, PlaceCategories.WantToGo, categoryAttractions, data);
+
+ LiveData liked = getRandomImagesForFlag(AttractionFlag.Option.Liked.code);
+ addSourceToRandomImagesLiveData(liked, PlaceCategories.Liked, categoryAttractions, data);
+
+ LiveData nature = getRandomNatureImages();
+ addSourceToRandomImagesLiveData(nature, PlaceCategories.Nature, categoryAttractions, data);
+
+ LiveData exercise = getRandomExerciseImages();
+ addSourceToRandomImagesLiveData(exercise, PlaceCategories.Exercise, categoryAttractions, data);
+
+ LiveData educational = getRandomExerciseImages();
+ addSourceToRandomImagesLiveData(educational, PlaceCategories.Educational, categoryAttractions, data);
+ return data;
+ }
+
+ /**
+ * Helper method to build a synthesized `LiveData` object containing all categories
+ * for the home page grid of random images.
+ *
+ * @param source A `LiveData` result to add to the set of `LiveData`
+ * @param category Which category this is for
+ * @param categoryAttractions Set of results being built
+ * @param data The `MediatorLiveData` to attach this source to
+ */
+ private void addSourceToRandomImagesLiveData(LiveData source,
+ PlaceCategories category,
+ ArrayList categoryAttractions,
+ MediatorLiveData> data) {
+
+ data.addSource(source, categoryImage -> {
+ if (categoryImage == null) {
+ return;
+ }
+ CategoryAttraction attraction = new CategoryAttraction(category.code, categoryImage.getImage());
+ categoryAttractions.set(category.code, attraction);
+ data.postValue(categoryAttractions);
+ data.removeSource(source);
+ });
+ }
+
/**
* Find those destinations with a given flag set, which are those that should be geofenced.
*
@@ -54,7 +157,7 @@ public void bulkUpdate(List destinations) {
*/
@Query(value = "SELECT destination.* FROM destination INNER JOIN attractionflag " +
- "ON destination.id = attractionflag.attractionID AND attractionflag.is_event = 0 " +
+ "ON destination.id = attractionflag.attraction_id AND attractionflag.is_event = 0 " +
"WHERE attractionflag.option = :geofenceFlagCode")
public abstract List getGeofenceDestinations(int geofenceFlagCode);
diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java
index c950c5df..11429b0f 100644
--- a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java
+++ b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java
@@ -7,6 +7,7 @@
import android.util.Log;
import com.gophillygo.app.data.models.AttractionFlag;
+import com.gophillygo.app.data.models.CategoryAttraction;
import com.gophillygo.app.data.models.Destination;
import com.gophillygo.app.data.models.DestinationInfo;
import com.gophillygo.app.data.models.Event;
@@ -32,7 +33,12 @@
* https://developer.android.com/topic/libraries/architecture/guide.html
*/
-class DestinationRepository {
+public class DestinationRepository {
+
+ public interface CategoryAttractionCallback {
+ void gotCategoryAttractions(LiveData> categoryAttractions);
+ }
+
private static final String LOG_LABEL = "DestinationRepository";
private final DestinationWebservice webservice;
@@ -108,7 +114,6 @@ public void onFailure(@NonNull Call call, @NonNull Throwab
Log.e(LOG_LABEL, "Request to POST user flag failed: " + t.toString());
}
});
-
}
@SuppressLint("StaticFieldLeak")
@@ -151,4 +156,21 @@ protected LiveData> loadFromDb() {
}
}.getAsLiveData();
}
+
+ @SuppressLint("StaticFieldLeak")
+ public void loadCategoryAttractions(CategoryAttractionCallback callback) {
+ Log.d(LOG_LABEL, "going to load category attractions");
+ new AsyncTask>>() {
+ @Override
+ protected void onPostExecute(LiveData> categories) {
+ Log.d(LOG_LABEL, "finished loading category attractions");
+ callback.gotCategoryAttractions(categories);
+ }
+
+ @Override
+ protected LiveData> doInBackground(Void... voids) {
+ return destinationDao.getCategoryImages();
+ }
+ }.execute();
+ }
}
diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java b/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java
index b65c2556..1c2852c8 100644
--- a/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java
+++ b/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java
@@ -3,6 +3,7 @@
import android.arch.lifecycle.LiveData;
import com.gophillygo.app.data.models.AttractionFlag;
+import com.gophillygo.app.data.models.CategoryAttraction;
import com.gophillygo.app.data.models.Destination;
import com.gophillygo.app.data.models.DestinationInfo;
import com.gophillygo.app.data.networkresource.Resource;
@@ -42,4 +43,8 @@ public void updateMultipleDestinations(List infos) {
public LiveData>> getDestinations() {
return destinations;
}
+
+ public void getCategoryAttractions(DestinationRepository.CategoryAttractionCallback listener) {
+ destinationRepository.loadCategoryAttractions(listener);
+ }
}
diff --git a/app/src/main/java/com/gophillygo/app/data/EventDao.java b/app/src/main/java/com/gophillygo/app/data/EventDao.java
index 2dc4b029..62baedb1 100644
--- a/app/src/main/java/com/gophillygo/app/data/EventDao.java
+++ b/app/src/main/java/com/gophillygo/app/data/EventDao.java
@@ -3,6 +3,7 @@
import android.arch.lifecycle.LiveData;
import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
@@ -17,13 +18,14 @@
@Dao
public abstract class EventDao implements AttractionDao {
+ @Transaction
@Query("SELECT event.*, destination.name AS destinationName, NULL AS distance, " +
"destination.categories AS destinationCategories, attractionflag.option, " +
"destination.x, destination.y, destination.distance " +
"FROM event " +
"LEFT JOIN destination ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " +
+ "ON event.id = attractionflag.attraction_id AND attractionflag.is_event = 1 " +
"ORDER BY event.start_date ASC;")
public abstract LiveData> getAll();
@@ -33,7 +35,7 @@ public abstract class EventDao implements AttractionDao {
"FROM event " +
"LEFT JOIN destination ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " +
+ "ON event.id = attractionflag.attraction_id AND attractionflag.is_event = 1 " +
"WHERE event.id = :eventId")
public abstract LiveData getEvent(long eventId);
@@ -62,7 +64,7 @@ public void bulkUpdate(List events) {
"FROM event " +
"INNER JOIN destination ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " +
+ "ON event.id = attractionflag.attraction_id AND attractionflag.is_event = 1 " +
"WHERE attractionflag.option = :geofenceFlagCode")
public abstract List getGeofenceEvents(int geofenceFlagCode);
@@ -78,7 +80,7 @@ public void bulkUpdate(List events) {
"FROM event " +
"LEFT JOIN destination ON destination.id = event.destination " +
"LEFT JOIN attractionflag " +
- "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " +
+ "ON event.id = attractionflag.attraction_id AND attractionflag.is_event = 1 " +
"WHERE event.id = :eventId")
public abstract EventInfo getEventInBackground(long eventId);
}
diff --git a/app/src/main/java/com/gophillygo/app/data/GpgDatabase.java b/app/src/main/java/com/gophillygo/app/data/GpgDatabase.java
index f78c5752..e922d8ec 100644
--- a/app/src/main/java/com/gophillygo/app/data/GpgDatabase.java
+++ b/app/src/main/java/com/gophillygo/app/data/GpgDatabase.java
@@ -8,7 +8,7 @@
import com.gophillygo.app.data.models.Destination;
import com.gophillygo.app.data.models.Event;
-@Database(version=10, entities={AttractionFlag.class, Destination.class, Event.class})
+@Database(version=11, entities={AttractionFlag.class, Destination.class, Event.class})
@TypeConverters({RoomConverters.class})
public abstract class GpgDatabase extends RoomDatabase {
abstract public DestinationDao destinationDao();
diff --git a/app/src/main/java/com/gophillygo/app/data/models/AttractionFlag.java b/app/src/main/java/com/gophillygo/app/data/models/AttractionFlag.java
index 6116d3b0..698e729f 100644
--- a/app/src/main/java/com/gophillygo/app/data/models/AttractionFlag.java
+++ b/app/src/main/java/com/gophillygo/app/data/models/AttractionFlag.java
@@ -2,6 +2,7 @@
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Index;
import android.arch.persistence.room.TypeConverter;
import android.arch.persistence.room.TypeConverters;
import android.support.annotation.DrawableRes;
@@ -13,7 +14,8 @@
import java.util.Objects;
-@Entity(primaryKeys = {"attractionID", "is_event"})
+@Entity(primaryKeys = {"attraction_id", "is_event"},
+ indices = {@Index(value = {"is_event", "option"})})
public class AttractionFlag {
public enum Option {
@@ -47,7 +49,7 @@ public static Option valueOf(int code) {
}
}
- @ColumnInfo(index = true)
+ @ColumnInfo(name = "attraction_id", index = true)
private final int attractionID;
@ColumnInfo(name = "is_event", index = true)
@@ -55,6 +57,7 @@ public static Option valueOf(int code) {
private final boolean isEvent;
@TypeConverters(OptionConverter.class)
+ @ColumnInfo(index = true)
private final Option option;
public AttractionFlag(int attractionID, boolean isEvent, Option option) {
@@ -99,7 +102,6 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
-
return Objects.hash(attractionID, isEvent, option);
}
}
diff --git a/app/src/main/java/com/gophillygo/app/data/models/CategoryAttraction.java b/app/src/main/java/com/gophillygo/app/data/models/CategoryAttraction.java
new file mode 100644
index 00000000..b5f14f56
--- /dev/null
+++ b/app/src/main/java/com/gophillygo/app/data/models/CategoryAttraction.java
@@ -0,0 +1,99 @@
+package com.gophillygo.app.data.models;
+
+import android.content.Context;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.util.SparseArray;
+
+import com.gophillygo.app.R;
+
+import java.util.Objects;
+
+import static com.gophillygo.app.data.models.Filter.EDUCATIONAL_CATEGORY;
+import static com.gophillygo.app.data.models.Filter.EXERCISE_CATEGORY;
+import static com.gophillygo.app.data.models.Filter.NATURE_CATEGORY;
+
+/**
+ * Model for the home screen categories, which display a random representative image from
+ * places of a given category.
+ */
+public class CategoryAttraction {
+
+ // category grid cards that are not in the destination category string: two user flags,
+ // and events
+ public static final String EVENTS_DB_NAME = "events";
+ public static final String NATURE_DB_NAME = "nature";
+ public static final String EXERCISE_DB_NAME = "exercise";
+ public static final String EDUCATIONAL_DB_NAME = "educational";
+
+
+ // FIXME: reference user-facing strings (displayName) as strings resources
+
+ public enum PlaceCategories {
+ Events(0, R.string.home_grid_events, EVENTS_DB_NAME),
+ WantToGo(1, R.string.place_want_to_go_option, AttractionFlag.Option.WantToGo.api_name),
+ Liked(2, R.string.place_liked_option, AttractionFlag.Option.Liked.api_name),
+ Nature(3, R.string.nature_category_label, NATURE_DB_NAME),
+ Exercise(4, R.string.exercise_category_label, EXERCISE_DB_NAME),
+ Educational(5, R.string.educational_category_label, EDUCATIONAL_DB_NAME);
+
+ private static final SparseArray map = new SparseArray<>();
+ static {
+ for (PlaceCategories category : PlaceCategories.values()) {
+ map.put(category.code, category);
+ }
+ }
+
+ public final int code;
+ public final @IdRes Integer displayName;
+ public final String dbName;
+
+ PlaceCategories(int code, Integer displayName, String dbName) {
+ this.code = code;
+ this.displayName = displayName;
+ this.dbName = dbName;
+ }
+
+ public static int size() {
+ return map.size();
+ }
+
+ public static PlaceCategories valueOf(int code) {
+ return map.get(code);
+ }
+ }
+
+ private final PlaceCategories category;
+ private final String image;
+
+ public CategoryAttraction(int code, String image) {
+ this.category = PlaceCategories.valueOf(code);
+ this.image = image;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public PlaceCategories getCategory() {
+ return category;
+ }
+
+ public Integer getDisplayName() {
+ return category.displayName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CategoryAttraction)) return false;
+ CategoryAttraction that = (CategoryAttraction) o;
+ return category == that.category &&
+ Objects.equals(image, that.image);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(category, image);
+ }
+}
diff --git a/app/src/main/java/com/gophillygo/app/data/models/CategoryImage.java b/app/src/main/java/com/gophillygo/app/data/models/CategoryImage.java
new file mode 100644
index 00000000..f7a2435c
--- /dev/null
+++ b/app/src/main/java/com/gophillygo/app/data/models/CategoryImage.java
@@ -0,0 +1,35 @@
+package com.gophillygo.app.data.models;
+
+import android.arch.persistence.room.ColumnInfo;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Minimal model for representative images chosen from a filter category.
+ */
+public class CategoryImage {
+
+ private final int attractionID;
+ @ColumnInfo(name = "is_event")
+ private final boolean isEvent;
+ private final String image;
+
+ public CategoryImage(int attractionID, boolean isEvent, String image) {
+
+ this.attractionID = attractionID;
+ this.isEvent = isEvent;
+ this.image = image;
+ }
+
+ public int getAttractionID() {
+ return attractionID;
+ }
+
+ public boolean isEvent() {
+ return isEvent;
+ }
+
+ public String getImage() {
+ return image;
+ }
+}
diff --git a/app/src/main/java/com/gophillygo/app/data/models/Destination.java b/app/src/main/java/com/gophillygo/app/data/models/Destination.java
index c2e21595..ccafaf94 100644
--- a/app/src/main/java/com/gophillygo/app/data/models/Destination.java
+++ b/app/src/main/java/com/gophillygo/app/data/models/Destination.java
@@ -4,6 +4,7 @@
import android.arch.persistence.room.Embedded;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Index;
import com.google.gson.annotations.SerializedName;
@@ -12,9 +13,11 @@
import java.util.Objects;
-@Entity(inheritSuperIndices = true)
+@Entity(inheritSuperIndices = true,
+ indices = {@Index("educational"), @Index("nature"), @Index("exercise")})
public class Destination extends Attraction {
+ private static final String LOG_LABEL = "DestinationModel";
private static final NumberFormat numberFormatter = NumberFormat.getNumberInstance();
static {
numberFormatter.setMinimumFractionDigits(0);
@@ -27,10 +30,14 @@ public class Destination extends Attraction {
private final ArrayList categories;
+ // convenience boolean flags for whether categories are set
+ @Embedded
+ private DestinationCategories categoryFlags;
+
@Embedded
private final DestinationLocation location;
- @ColumnInfo(name = "watershed_alliance")
+ @ColumnInfo(name = "watershed_alliance", index = true)
@SerializedName("watershed_alliance")
private final boolean watershedAlliance;
@@ -42,6 +49,7 @@ public class Destination extends Attraction {
private final String zipCode;
// convenience property to track distance to each destination
+ @ColumnInfo(index = true)
private float distance;
@Ignore
private String formattedDistance;
@@ -70,9 +78,14 @@ public Destination(int id, int placeID, String name, boolean accessible, String
public void setDistance(float distance) {
this.distance = distance;
+ // FIXME: move string constant to resources
this.formattedDistance = numberFormatter.format(distance) + " mi";
}
+ public void setCategoryFlags(DestinationCategories categoryFlags) {
+ this.categoryFlags = categoryFlags;
+ }
+
public String getCity() {
return city;
}
@@ -113,6 +126,10 @@ public ArrayList getCategories() {
return categories;
}
+ public DestinationCategories getCategoryFlags() {
+ return categoryFlags;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/app/src/main/java/com/gophillygo/app/data/models/DestinationCategories.java b/app/src/main/java/com/gophillygo/app/data/models/DestinationCategories.java
new file mode 100644
index 00000000..2ec7074a
--- /dev/null
+++ b/app/src/main/java/com/gophillygo/app/data/models/DestinationCategories.java
@@ -0,0 +1,54 @@
+package com.gophillygo.app.data.models;
+
+import java.util.Objects;
+
+public class DestinationCategories {
+ private boolean nature;
+ private boolean exercise;
+ private boolean educational;
+
+ public DestinationCategories(boolean nature, boolean exercise, boolean educational) {
+ this.nature = nature;
+ this.exercise = exercise;
+ this.educational = educational;
+ }
+
+ public boolean isNature() {
+ return nature;
+ }
+
+ public boolean isExercise() {
+ return exercise;
+ }
+
+ public boolean isEducational() {
+ return educational;
+ }
+
+ public void setNature(boolean nature) {
+ this.nature = nature;
+ }
+
+ public void setExercise(boolean exercise) {
+ this.exercise = exercise;
+ }
+
+ public void setEducational(boolean educational) {
+ this.educational = educational;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DestinationCategories)) return false;
+ DestinationCategories that = (DestinationCategories) o;
+ return nature == that.nature &&
+ exercise == that.exercise &&
+ educational == that.educational;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nature, exercise, educational);
+ }
+}
diff --git a/app/src/main/java/com/gophillygo/app/data/models/EventInfo.java b/app/src/main/java/com/gophillygo/app/data/models/EventInfo.java
index 8a9fee01..7567ee86 100644
--- a/app/src/main/java/com/gophillygo/app/data/models/EventInfo.java
+++ b/app/src/main/java/com/gophillygo/app/data/models/EventInfo.java
@@ -1,10 +1,12 @@
package com.gophillygo.app.data.models;
+import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Embedded;
import android.arch.persistence.room.Ignore;
import java.text.NumberFormat;
import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
public class EventInfo extends AttractionInfo {
@@ -20,10 +22,17 @@ public class EventInfo extends AttractionInfo {
// fetch fields of related destination from database into these properties
private final String destinationName;
+
private final ArrayList destinationCategories;
+ @Embedded
+ @Ignore
+ private final DestinationCategories categories;
+
@Embedded
private final DestinationLocation location;
+
+ @ColumnInfo(index = true)
private final Float distance;
@Ignore
private final String formattedDistance;
@@ -41,6 +50,14 @@ public EventInfo(Event event, String destinationName, ArrayList destinat
} else {
this.formattedDistance = "";
}
+
+ if (destinationCategories != null && !destinationCategories.isEmpty()) {
+ this.categories = new DestinationCategories(destinationCategories.contains(Filter.NATURE_CATEGORY),
+ destinationCategories.contains(Filter.EXERCISE_CATEGORY),
+ destinationCategories.contains(Filter.EDUCATIONAL_CATEGORY));
+ } else {
+ this.categories = new DestinationCategories(false, false, false);
+ }
}
@Override
@@ -79,6 +96,10 @@ public ArrayList getDestinationCategories() {
return destinationCategories;
}
+ public DestinationCategories getCategories() {
+ return categories;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/app/src/main/java/com/gophillygo/app/data/models/Filter.java b/app/src/main/java/com/gophillygo/app/data/models/Filter.java
index 24b60d64..42965657 100644
--- a/app/src/main/java/com/gophillygo/app/data/models/Filter.java
+++ b/app/src/main/java/com/gophillygo/app/data/models/Filter.java
@@ -2,15 +2,19 @@
import android.databinding.BaseObservable;
import android.databinding.Bindable;
+import android.util.Log;
+
+import com.gophillygo.app.BR;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import com.gophillygo.app.BR;
-
public class Filter extends BaseObservable implements Serializable {
+
+ private static final String LOG_LABEL = "Filter";
+
public static final String NATURE_CATEGORY = "Nature";
public static final String EXERCISE_CATEGORY = "Exercise";
public static final String EDUCATIONAL_CATEGORY = "Educational";
@@ -76,7 +80,12 @@ public void reset() {
}
public boolean matches(DestinationInfo info) {
- boolean categoryMatches = categoryMatches(info.getDestination().getCategories());
+ DestinationCategories flags = info.getDestination().getCategoryFlags();
+ if (flags == null) {
+ Log.e(LOG_LABEL, "Category flags are missing for destination " + info.getDestination().getName());
+ return false;
+ }
+ boolean categoryMatches = categoryMatches(flags);
boolean flagMatches = flagMatches(info.getFlag());
boolean accessibleMatches = accessibleMatches(info.getDestination().isAccessible());
@@ -84,25 +93,21 @@ public boolean matches(DestinationInfo info) {
}
public boolean matches(EventInfo info) {
- boolean categoryMatches = categoryMatches(info.getDestinationCategories());
+ boolean categoryMatches = categoryMatches(info.getCategories());
boolean flagMatches = flagMatches(info.getFlag());
boolean accessibleMatches = accessibleMatches(info.getEvent().isAccessible());
return categoryMatches && flagMatches && accessibleMatches;
}
- private boolean categoryMatches(List destCategories) {
- boolean categoryMatches = categories().isEmpty();
- if (destCategories == null) {
- return categoryMatches;
- }
-
- for (String category : categories()) {
- if (destCategories.contains(category)) {
- categoryMatches = true;
- }
+ private boolean categoryMatches(DestinationCategories categories) {
+ // match all if not filtering by category
+ if (!nature && !exercise && !educational) {
+ return true;
}
- return categoryMatches;
+ // match on any filter category
+ return ((nature && categories.isNature()) || (educational && categories.isEducational()) ||
+ (exercise && categories.isExercise()));
}
private boolean flagMatches(AttractionFlag flag) {
@@ -123,21 +128,6 @@ private boolean accessibleMatches(boolean isAccessible) {
return accessibleMatches;
}
- private List categories() {
- List categories = new ArrayList<>();
- if (nature) {
- categories.add(NATURE_CATEGORY);
- }
- if (exercise) {
- categories.add(EXERCISE_CATEGORY);
- }
- if (educational) {
- categories.add(EDUCATIONAL_CATEGORY);
- }
- return categories;
-
- }
-
private List flags() {
List flags = new ArrayList<>();
if (been) {
diff --git a/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java b/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java
index 59fa4436..f78020e0 100644
--- a/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java
+++ b/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java
@@ -10,8 +10,10 @@
import com.gophillygo.app.data.models.Attraction;
import com.gophillygo.app.data.models.AttractionInfo;
import com.gophillygo.app.data.models.Destination;
+import com.gophillygo.app.data.models.DestinationCategories;
import com.gophillygo.app.data.models.DestinationQueryResponse;
import com.gophillygo.app.data.models.Event;
+import com.gophillygo.app.data.models.Filter;
import java.util.HashSet;
import java.util.List;
@@ -54,6 +56,14 @@ protected void saveCallResult(@NonNull DestinationQueryResponse response) {
Set destinationIds = new HashSet<>(destinations.size());
for (Destination item : destinations) {
item.setTimestamp(timestamp);
+ List categories = item.getCategories();
+ if (categories != null && !categories.isEmpty()) {
+ item.setCategoryFlags(new DestinationCategories(categories.contains(Filter.NATURE_CATEGORY),
+ categories.contains(Filter.EXERCISE_CATEGORY),
+ categories.contains(Filter.EDUCATIONAL_CATEGORY)));
+ } else {
+ item.setCategoryFlags(new DestinationCategories(false, false, false));
+ }
destinationIds.add(item.getId());
destinationDao.save(item);
}
diff --git a/app/src/main/java/com/gophillygo/app/GlideImageDataBinding.java b/app/src/main/java/com/gophillygo/app/utils/GlideImageDataBinding.java
similarity index 93%
rename from app/src/main/java/com/gophillygo/app/GlideImageDataBinding.java
rename to app/src/main/java/com/gophillygo/app/utils/GlideImageDataBinding.java
index 66591263..5b118b21 100644
--- a/app/src/main/java/com/gophillygo/app/GlideImageDataBinding.java
+++ b/app/src/main/java/com/gophillygo/app/utils/GlideImageDataBinding.java
@@ -1,4 +1,4 @@
-package com.gophillygo.app;
+package com.gophillygo.app.utils;
import android.content.Context;
import android.databinding.BindingAdapter;
diff --git a/app/src/main/res/layout/activity_events_list.xml b/app/src/main/res/layout/activity_events_list.xml
index ae7b3a62..fc872f92 100644
--- a/app/src/main/res/layout/activity_events_list.xml
+++ b/app/src/main/res/layout/activity_events_list.xml
@@ -16,6 +16,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
+ android:minHeight="?attr/actionBarSize"
android:fitsSystemWindows="true"
android:elevation="0dp"
android:background="@color/color_primary"
@@ -41,8 +42,11 @@
android:id="@+id/empty_events_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:gravity="center"
android:visibility="gone"
+ android:layout_marginTop="20dp"
+ android:textAlignment="center"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
+ android:layout_below="@+id/events_list_filter_button_bar"
android:text="@string/filter_no_results" />
diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml
index d89f771c..9353f768 100644
--- a/app/src/main/res/layout/activity_home.xml
+++ b/app/src/main/res/layout/activity_home.xml
@@ -1,51 +1,54 @@
-
-
-
+ tools:context="com.gophillygo.app.activities.HomeActivity"
+ android:id="@+id/relativeLayout">
-
+ android:layout_width="match_parent" >
-
+
+
+
-
+
+
+
diff --git a/app/src/main/res/layout/activity_places_list.xml b/app/src/main/res/layout/activity_places_list.xml
index 0cdd2081..0def699b 100644
--- a/app/src/main/res/layout/activity_places_list.xml
+++ b/app/src/main/res/layout/activity_places_list.xml
@@ -3,47 +3,62 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.gophillygo.app.activities.PlacesListActivity">
+
-
+ android:layout_height="wrap_content">
-
+ app:layout_constraintBottom_toTopOf="@+id/places_list_recycler_view"
+ app:layout_constraintTop_toBottomOf="@+id/places_list_filter_button_bar" />
+
-
-
+
-
+
+
-
diff --git a/app/src/main/res/layout/place_category_grid_item.xml b/app/src/main/res/layout/place_category_grid_item.xml
index a870c292..d9d334ab 100644
--- a/app/src/main/res/layout/place_category_grid_item.xml
+++ b/app/src/main/res/layout/place_category_grid_item.xml
@@ -1,27 +1,44 @@
-
+
-
+
+
+
+
+
+
+ android:gravity="center"
+ android:id="@+id/place_category_grid_relative_layout">
+
+
-
+
-
+
+
diff --git a/app/src/main/res/layout/place_list_item.xml b/app/src/main/res/layout/place_list_item.xml
index 2fa66591..c983857e 100644
--- a/app/src/main/res/layout/place_list_item.xml
+++ b/app/src/main/res/layout/place_list_item.xml
@@ -1,5 +1,7 @@
@@ -16,11 +18,15 @@
android:paddingBottom="10dp"
android:layout_margin="10dp">
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 817df928..7ea3f318 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -90,4 +90,5 @@
Please set location mode to \"High Accuracy\" to receive notifications when places you want to go are nearby.
Nearby GoPhillyGo destinations you want to go visit
Are you visiting %1$s now? Learn more!
+ Upcoming events