From 081124b089d76aaee2cd5184eb99a81f61824a52 Mon Sep 17 00:00:00 2001 From: lamessk Date: Tue, 27 Nov 2018 10:57:04 -0700 Subject: [PATCH 1/8] -add firebase json --- .idea/caches/build_file_checksums.ser | Bin 536 -> 537 bytes app/google-services.json | 42 ++++++++++++++++++ .../googlemaps2018/ui/ChatroomActivity.java | 1 + build.gradle | 2 +- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 app/google-services.json diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 2323845d656313ae5c18d32ff3f9c20e9c7d6e76..1ea4774c4337be9da13aa7caa74263f1bb4c4898 100644 GIT binary patch delta 98 zcmV-o0G zb6iYeZDn(lUja=J5g6h|-}VrGO*fp>sngv@le_^P5auBgnfcYF zb6j-YGRy#XB%hxIj+O=A|V=kL^dk4Uk_lj;F4 Dk;f_O diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..ffbc148 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,42 @@ +{ + "project_info": { + "project_number": "999529025534", + "firebase_url": "https://maps-2018-a3040.firebaseio.com", + "project_id": "maps-2018-a3040", + "storage_bucket": "maps-2018-a3040.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:999529025534:android:a1d1ce9b8c3681aa", + "android_client_info": { + "package_name": "com.codingwithmitch.googlemaps2018" + } + }, + "oauth_client": [ + { + "client_id": "999529025534-phqa0i38lbvdan5ur18ii05lhopd0431.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCYuRqkzUIhxXRBSHWTh87a0rRFq227M9M" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 1, + "other_platform_oauth_client": [] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java index b7deaf5..431a394 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java @@ -12,6 +12,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.WindowManager; import android.widget.EditText; import com.codingwithmitch.googlemaps2018.R; diff --git a/build.gradle b/build.gradle index 05da648..a8204d7 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.1.4' classpath 'com.google.gms:google-services:4.0.1' // NOTE: Do not place your application dependencies here; they belong From dfbe996c159a4ee8e07722a3b2493c890953e54a Mon Sep 17 00:00:00 2001 From: lamessk Date: Tue, 27 Nov 2018 11:42:22 -0700 Subject: [PATCH 2/8] -Added Google Maps Permissions and SDK --- .idea/caches/build_file_checksums.ser | Bin 537 -> 537 bytes app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 22 +++ .../googlemaps2018/Constants.java | 2 + .../googlemaps2018/ui/MainActivity.java | 129 +++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 6 files changed, 157 insertions(+), 2 deletions(-) diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 1ea4774c4337be9da13aa7caa74263f1bb4c4898..9659bc00b79b7f4f71492390d4485c6a7aa679c8 100644 GIT binary patch delta 34 scmV+-0NwwY1epYonFCj!ypf!D5JtNrxOf>4wFv18r8a8*+LI#z_Vx-6!~g&Q delta 34 scmV+-0NwwY1epYonFBl41(BS05a&gzj4kYUqZ%;oZ02?pKa(Q?_WHLD#sB~S diff --git a/app/build.gradle b/app/build.gradle index fb0608a..16e8090 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,5 +45,10 @@ dependencies { // Circle ImageView implementation 'de.hdodenhof:circleimageview:2.2.0' + + //Google services + implementation 'com.google.android.gms:play-services-maps:15.0.1' + implementation 'com.google.android.gms:play-services-places:15.0.1' + implementation 'com.google.android.gms:play-services-location:15.0.1' } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2063a53..087540c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java index 0534474..6f9cec7 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java @@ -3,4 +3,6 @@ public class Constants { public static final int ERROR_DIALOG_REQUEST = 9001; + public static final int PERMISSIONS_REQUEST_ENABLE_GPS = 9002; + public static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 9003; } diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java index 904dfb1..bf82a08 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java @@ -5,10 +5,13 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.location.LocationManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -46,12 +49,13 @@ import javax.annotation.Nullable; import static com.codingwithmitch.googlemaps2018.Constants.ERROR_DIALOG_REQUEST; +import static com.codingwithmitch.googlemaps2018.Constants.PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION; +import static com.codingwithmitch.googlemaps2018.Constants.PERMISSIONS_REQUEST_ENABLE_GPS; public class MainActivity extends AppCompatActivity implements View.OnClickListener, - ChatroomRecyclerAdapter.ChatroomRecyclerClickListener -{ + ChatroomRecyclerAdapter.ChatroomRecyclerClickListener { private static final String TAG = "MainActivity"; @@ -65,6 +69,7 @@ public class MainActivity extends AppCompatActivity implements private RecyclerView mChatroomRecyclerView; private ListenerRegistration mChatroomEventListener; private FirebaseFirestore mDb; + private boolean mLocationPermissionGranted = false; @Override protected void onCreate(Bundle savedInstanceState) { @@ -81,6 +86,118 @@ protected void onCreate(Bundle savedInstanceState) { initChatroomRecyclerView(); } + private boolean checkMapServices(){ + if(isServicesOK()){ + if(isMapsEnabled()){ + return true; + } + } + return false; + } + + //Gets users to enable GPS in their settings + //Tells us if user enabled permissions or not + private void buildAlertMessageNoGps() { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage("This application requires GPS to work properly, do you want to enable it?") + .setCancelable(false) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + public void onClick(@SuppressWarnings("unused") final DialogInterface dialog, @SuppressWarnings("unused") final int id) { + Intent enableGpsIntent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS); + startActivityForResult(enableGpsIntent, PERMISSIONS_REQUEST_ENABLE_GPS); + } + }); + final AlertDialog alert = builder.create(); + alert.show(); + } + + public boolean isMapsEnabled(){ + final LocationManager manager = (LocationManager) getSystemService( Context.LOCATION_SERVICE ); + + if ( !manager.isProviderEnabled( LocationManager.GPS_PROVIDER ) ) { + buildAlertMessageNoGps(); + return false; + } + return true; + } + + //If permission granted, get chatrooms + //Otherwise, prompt to ask if it's fine to use location + private void getLocationPermission() { + /* + * Request location permission, so that we can get the location of the + * device. The result of the permission request is handled by a callback, + * onRequestPermissionsResult. + */ + if (ContextCompat.checkSelfPermission(this.getApplicationContext(), + android.Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + mLocationPermissionGranted = true; + getChatrooms(); + } else { + ActivityCompat.requestPermissions(this, + new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, + PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION); + } + } + + public boolean isServicesOK(){ + Log.d(TAG, "isServicesOK: checking google services version"); + + int available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(MainActivity.this); + + if(available == ConnectionResult.SUCCESS){ + //everything is fine and the user can make map requests + Log.d(TAG, "isServicesOK: Google Play Services is working"); + return true; + } + else if(GoogleApiAvailability.getInstance().isUserResolvableError(available)){ + //an error occured but we can resolve it + Log.d(TAG, "isServicesOK: an error occured but we can fix it"); + Dialog dialog = GoogleApiAvailability.getInstance().getErrorDialog(MainActivity.this, available, ERROR_DIALOG_REQUEST); + dialog.show(); + }else{ + Toast.makeText(this, "You can't make map requests", Toast.LENGTH_SHORT).show(); + } + return false; + } + + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String permissions[], + @NonNull int[] grantResults) { + mLocationPermissionGranted = false; + switch (requestCode) { + case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + mLocationPermissionGranted = true; + } + } + } + } + + //Check for GPS Permission + //Opens chatrooms if they accepted + //Otherwise ask for the permissions again + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d(TAG, "onActivityResult: called."); + switch (requestCode) { + case PERMISSIONS_REQUEST_ENABLE_GPS: { + if(mLocationPermissionGranted){ + getChatrooms(); + } + else{ + getLocationPermission(); + } + } + } + + } + private void initSupportActionBar(){ setTitle("Chatrooms"); } @@ -218,6 +335,14 @@ protected void onDestroy() { protected void onResume() { super.onResume(); getChatrooms(); + if(checkMapServices()){ + if(mLocationPermissionGranted){ + getChatrooms(); + } + else{ + getLocationPermission(); + } + } } @Override diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b085577..4fa8a98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,5 +49,6 @@ User list Profile + AIzaSyB8Ygc0PYpUHGrovc3p6q6sE8UdZDZJMY4 From 73a430f909617d7addc2f2805f3d6e61fe796313 Mon Sep 17 00:00:00 2001 From: lamessk Date: Tue, 27 Nov 2018 18:34:49 -0700 Subject: [PATCH 3/8] -Map shows cluster of pins stored in DB --- .idea/caches/build_file_checksums.ser | Bin 537 -> 537 bytes app/build.gradle | 4 + .../googlemaps2018/Constants.java | 2 + .../googlemaps2018/UserClient.java | 5 + .../googlemaps2018/models/ClusterMarker.java | 61 ++++++ .../googlemaps2018/models/MyItem.java | 35 ++++ .../googlemaps2018/models/UserLocation.java | 86 ++++++++ .../googlemaps2018/ui/ChatroomActivity.java | 25 ++- .../googlemaps2018/ui/LoginActivity.java | 10 +- .../googlemaps2018/ui/MainActivity.java | 90 +++++++++ .../googlemaps2018/ui/UserListFragment.java | 189 +++++++++++++++++- .../util/MyClusterManagerRenderer.java | 67 +++++++ .../main/res/layout/fragment_user_list.xml | 42 +++- app/src/main/res/values/dimens.xml | 5 + build.gradle | 3 + 15 files changed, 603 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/models/ClusterMarker.java create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/models/MyItem.java create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/models/UserLocation.java create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/util/MyClusterManagerRenderer.java create mode 100644 app/src/main/res/values/dimens.xml diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index 9659bc00b79b7f4f71492390d4485c6a7aa679c8..d6ff25bddd97daead506f251784077a78a9427c1 100644 GIT binary patch delta 56 zcmV-80LTBC1epYom;_gJgVm9ocM#No=|aJ4v@;bfdFJ->#&VM*0g4cB88%m>D~PPk O;5T=(AMy5+y#ZY1N*cQW delta 56 zcmV-80LTBC1epYom;_c=pS+QrcMwLqBe-}O548yC3#B${{@Rlx0g4b27~)3X_7Hwe OH=NX|)7?jty#ZXDY8bNs diff --git a/app/build.gradle b/app/build.gradle index 16e8090..ebbafde 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,10 @@ dependencies { // Circle ImageView implementation 'de.hdodenhof:circleimageview:2.2.0' + // Google marker cluster + implementation'com.google.maps.android:android-maps-utils:0.5+' + + //Google services implementation 'com.google.android.gms:play-services-maps:15.0.1' implementation 'com.google.android.gms:play-services-places:15.0.1' diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java index 6f9cec7..c63171d 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/Constants.java @@ -5,4 +5,6 @@ public class Constants { public static final int ERROR_DIALOG_REQUEST = 9001; public static final int PERMISSIONS_REQUEST_ENABLE_GPS = 9002; public static final int PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 9003; + public static final String MAPVIEW_BUNDLE_KEY = "MapViewBundleKey"; + } diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/UserClient.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/UserClient.java index becc91e..a6eb359 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/UserClient.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/UserClient.java @@ -7,12 +7,17 @@ public class UserClient extends Application { + //Extends applications, means we can have only a single user logged in + //and authenticated at any given time + //Users are unique and do not change very often + private User user = null; public User getUser() { return user; } + //Set as soon as user is authenticated public void setUser(User user) { this.user = user; } diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/models/ClusterMarker.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/ClusterMarker.java new file mode 100644 index 0000000..cb10179 --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/ClusterMarker.java @@ -0,0 +1,61 @@ +package com.codingwithmitch.googlemaps2018.models; + +import com.google.android.gms.maps.model.LatLng; +import com.google.maps.android.clustering.ClusterItem; + +public class ClusterMarker implements ClusterItem { + + private LatLng position; // required field + private String title; // required field + private String snippet; // required field + private int iconPicture; + private User user; + + public ClusterMarker(LatLng position, String title, String snippet, int iconPicture, User user) { + this.position = position; + this.title = title; + this.snippet = snippet; + this.iconPicture = iconPicture; + this.user = user; + } + + public int getIconPicture() { + return iconPicture; + } + + public void setIconPicture(int iconPicture) { + this.iconPicture = iconPicture; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public void setPosition(LatLng position) { + this.position = position; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setSnippet(String snippet) { + this.snippet = snippet; + } + + public LatLng getPosition() { + return position; + } + + public String getTitle() { + return title; + } + + public String getSnippet() { + return snippet; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/models/MyItem.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/MyItem.java new file mode 100644 index 0000000..d03e5ff --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/MyItem.java @@ -0,0 +1,35 @@ +package com.codingwithmitch.googlemaps2018.models; + +import com.google.android.gms.maps.model.LatLng; +import com.google.maps.android.clustering.ClusterItem; + +public class MyItem implements ClusterItem { + private LatLng mPosition; + private String mTitle; + private String mSnippet; + + public MyItem(double lat, double lng, String title, String snippet) { + mPosition = new LatLng(lat, lng); + mTitle = title; + mSnippet = snippet; + } + + public MyItem(double lat, double lng) { + mPosition = new LatLng(lat, lng); + } + + @Override + public LatLng getPosition() { + return mPosition; + } + + @Override + public String getTitle() { + return mTitle; + } + + @Override + public String getSnippet() { + return mSnippet; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/models/UserLocation.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/UserLocation.java new file mode 100644 index 0000000..af69c46 --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/UserLocation.java @@ -0,0 +1,86 @@ +package com.codingwithmitch.googlemaps2018.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.firebase.firestore.GeoPoint; +import com.google.firebase.firestore.ServerTimestamp; + +import java.util.Date; + +public class UserLocation implements Parcelable{ + + private GeoPoint geo_point; + //If you pass null at insertion, it will automatically insert a timestamp + private @ServerTimestamp Date timestamp; + private User user; + + public UserLocation(GeoPoint geo_point, Date timestamp, User user){ + this.geo_point = geo_point; + this.timestamp = timestamp; + this.user = user; + } + + public UserLocation(){ + + } + + protected UserLocation(Parcel in) { + user = in.readParcelable(User.class.getClassLoader()); + } + + public static final Creator CREATOR = new Creator() { + @Override + public UserLocation createFromParcel(Parcel in) { + return new UserLocation(in); + } + + @Override + public UserLocation[] newArray(int size) { + return new UserLocation[size]; + } + }; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public GeoPoint getGeo_point() { + return geo_point; + } + + public void setGeo_point(GeoPoint geo_point) { + this.geo_point = geo_point; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "UserLocation{" + + "user=" + user + + ", geo_point=" + geo_point + + ", timestamp=" + timestamp + + '}'; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(user, i); + } +} diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java index 431a394..e0035a1 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/ChatroomActivity.java @@ -21,11 +21,13 @@ import com.codingwithmitch.googlemaps2018.models.ChatMessage; import com.codingwithmitch.googlemaps2018.models.Chatroom; import com.codingwithmitch.googlemaps2018.models.User; +import com.codingwithmitch.googlemaps2018.models.UserLocation; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; @@ -57,7 +59,7 @@ public class ChatroomActivity extends AppCompatActivity implements private Set mMessageIds = new HashSet<>(); private ArrayList mUserList = new ArrayList<>(); private UserListFragment mUserListFragment; - + private ArrayList mUserLocations = new ArrayList<>(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -135,6 +137,7 @@ public void onEvent(@javax.annotation.Nullable QuerySnapshot queryDocumentSnapsh for (QueryDocumentSnapshot doc : queryDocumentSnapshots) { User user = doc.toObject(User.class); mUserList.add(user); + getUserLocation(user); } Log.d(TAG, "onEvent: user list size: " + mUserList.size()); @@ -143,6 +146,25 @@ public void onEvent(@javax.annotation.Nullable QuerySnapshot queryDocumentSnapsh }); } + private void getUserLocation(User user){ + //Retreive geopoint for each individual user and put into a list + + DocumentReference locationRef = mDb.collection(getString(R.string.collection_user_locations)) + .document(user.getUser_id()); + + locationRef.get().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(task.isSuccessful()){ + if(task.getResult().toObject(UserLocation.class) != null ){ + mUserLocations.add(task.getResult().toObject(UserLocation.class)); + } + } + + } + }); + } + private void initChatroomRecyclerView(){ mChatMessageRecyclerAdapter = new ChatMessageRecyclerAdapter(mMessages, new ArrayList(), this); mChatMessageRecyclerView.setAdapter(mChatMessageRecyclerAdapter); @@ -215,6 +237,7 @@ private void inflateUserListFragment(){ UserListFragment fragment = UserListFragment.newInstance(); Bundle bundle = new Bundle(); bundle.putParcelableArrayList(getString(R.string.intent_user_list), mUserList); + bundle.putParcelableArrayList(getString(R.string.intent_user_locations), mUserLocations); fragment.setArguments(bundle); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/LoginActivity.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/LoginActivity.java index bd164fc..25a538f 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/LoginActivity.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/LoginActivity.java @@ -56,8 +56,6 @@ protected void onCreate(Bundle savedInstanceState) { } - - private void showDialog(){ mProgressBar.setVisibility(View.VISIBLE); @@ -87,7 +85,7 @@ public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); Toast.makeText(LoginActivity.this, "Authenticated with: " + user.getEmail(), Toast.LENGTH_SHORT).show(); - FirebaseFirestore db = FirebaseFirestore.getInstance(); + /* FirebaseFirestore db = FirebaseFirestore.getInstance(); FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder() .setTimestampsInSnapshotsEnabled(true) .build(); @@ -102,10 +100,14 @@ public void onComplete(@NonNull Task task) { if(task.isSuccessful()){ Log.d(TAG, "onComplete: successfully set the user client."); User user = task.getResult().toObject(User.class); + //Single user client set here after authentication ((UserClient)(getApplicationContext())).setUser(user); + + + } } - }); + });*/ Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java index bf82a08..5f3210d 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/MainActivity.java @@ -1,11 +1,13 @@ package com.codingwithmitch.googlemaps2018.ui; +import android.Manifest; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; +import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.support.annotation.NonNull; @@ -25,23 +27,32 @@ import android.widget.Toast; import com.codingwithmitch.googlemaps2018.R; +import com.codingwithmitch.googlemaps2018.UserClient; import com.codingwithmitch.googlemaps2018.adapters.ChatroomRecyclerAdapter; import com.codingwithmitch.googlemaps2018.models.Chatroom; +import com.codingwithmitch.googlemaps2018.models.User; +import com.codingwithmitch.googlemaps2018.models.UserLocation; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreSettings; +import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.QueryDocumentSnapshot; import com.google.firebase.firestore.QuerySnapshot; +import org.w3c.dom.Document; + import java.util.ArrayList; import java.util.HashSet; import java.util.Set; @@ -70,6 +81,8 @@ public class MainActivity extends AppCompatActivity implements private ListenerRegistration mChatroomEventListener; private FirebaseFirestore mDb; private boolean mLocationPermissionGranted = false; + private FusedLocationProviderClient mFusedLocationClient; + private UserLocation mUserLocation; @Override protected void onCreate(Bundle savedInstanceState) { @@ -80,12 +93,86 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.fab_create_chatroom).setOnClickListener(this); + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this); + mDb = FirebaseFirestore.getInstance(); initSupportActionBar(); initChatroomRecyclerView(); } + //Step 1: Get User Details + private void getUserDetails(){ + if(mUserLocation == null) { + mUserLocation = new UserLocation(); + + //Get document referenced by User Id + DocumentReference userRef = mDb.collection(getString(R.string.collection_users)) + .document(FirebaseAuth.getInstance().getUid()); + + userRef.get().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "onComplete: successfully set the user client."); + User user = task.getResult().toObject(User.class); + mUserLocation.setUser(user); + ((UserClient) getApplicationContext()).setUser(user); + getLastKnownLocation(); + } + } + }); + } + else{ + getLastKnownLocation(); + } + } + + + //Step 3: Upload GPS coordinates to firestore for user + private void saveUserLocation(){ + + if(mUserLocation != null){ + DocumentReference locationRef = mDb + .collection(getString(R.string.collection_user_locations)) + .document(FirebaseAuth.getInstance().getUid()); + locationRef.set(mUserLocation).addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(task.isSuccessful()){ + Log.d(TAG, "saveUserLocation: \ninserted user location into database." + + "\n latitude: " + mUserLocation.getGeo_point().getLatitude() + + "\n longitude: " + mUserLocation.getGeo_point().getLongitude()); + } + } + }); + } + } + + + //Step2: Gets GPS coordinates of Users last known location + private void getLastKnownLocation() { + Log.d(TAG, "getLastKnownLocation: called."); + + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + return; + } + mFusedLocationClient.getLastLocation().addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if(task.isSuccessful()){ + Location location = task.getResult(); + GeoPoint geoPoint = new GeoPoint(location.getLatitude(), location.getLongitude()); + + mUserLocation.setGeo_point(geoPoint); + mUserLocation.setTimestamp(null); + saveUserLocation(); + } + } + }); + + } + private boolean checkMapServices(){ if(isServicesOK()){ if(isMapsEnabled()){ @@ -134,6 +221,7 @@ private void getLocationPermission() { == PackageManager.PERMISSION_GRANTED) { mLocationPermissionGranted = true; getChatrooms(); + getUserDetails( ); } else { ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, @@ -189,6 +277,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { case PERMISSIONS_REQUEST_ENABLE_GPS: { if(mLocationPermissionGranted){ getChatrooms(); + getUserDetails(); } else{ getLocationPermission(); @@ -338,6 +427,7 @@ protected void onResume() { if(checkMapServices()){ if(mLocationPermissionGranted){ getChatrooms(); + getUserDetails(); } else{ getLocationPermission(); diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java index 61c4ef1..0d00065 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java @@ -1,11 +1,16 @@ package com.codingwithmitch.googlemaps2018.ui; +import android.Manifest; +import android.content.pm.PackageManager; +import android.graphics.Camera; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,68 +18,232 @@ import com.codingwithmitch.googlemaps2018.R; import com.codingwithmitch.googlemaps2018.adapters.UserRecyclerAdapter; +import com.codingwithmitch.googlemaps2018.models.ClusterMarker; +import com.codingwithmitch.googlemaps2018.models.MyItem; import com.codingwithmitch.googlemaps2018.models.User; +import com.codingwithmitch.googlemaps2018.models.UserLocation; +import com.codingwithmitch.googlemaps2018.util.MyClusterManagerRenderer; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.MapView; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.firebase.auth.FirebaseAuth; +import com.google.maps.android.clustering.ClusterManager; +import com.google.maps.android.clustering.view.ClusterRenderer; import java.util.ArrayList; -public class UserListFragment extends Fragment { +import static com.codingwithmitch.googlemaps2018.Constants.MAPVIEW_BUNDLE_KEY; + +public class UserListFragment extends Fragment implements OnMapReadyCallback { private static final String TAG = "UserListFragment"; //widgets private RecyclerView mUserListRecyclerView; + private MapView mMapView; //vars private ArrayList mUserList = new ArrayList<>(); + private ArrayList mUserLocations = new ArrayList<>(); private UserRecyclerAdapter mUserRecyclerAdapter; + private GoogleMap mGoogleMap; + private LatLngBounds mMapBoundary; + private UserLocation mUserPosition; + private ClusterManager mClusterManager; + private MyClusterManagerRenderer mClusterManagerRenderer; + private ArrayList mClusterMarkers = new ArrayList<>(); - public static UserListFragment newInstance(){ + public static UserListFragment newInstance() { return new UserListFragment(); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if(getArguments() != null){ + if (getArguments() != null) { mUserList = getArguments().getParcelableArrayList(getString(R.string.intent_user_list)); + mUserLocations = getArguments().getParcelableArrayList(getString(R.string.intent_user_locations)); } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_user_list, container, false); + View view = inflater.inflate(R.layout.fragment_user_list, container, false); mUserListRecyclerView = view.findViewById(R.id.user_list_recycler_view); + mMapView = (MapView) view.findViewById(R.id.user_list_map); initUserListRecyclerView(); + initGoogleMap(savedInstanceState); + + setUserPosition(); + return view; } - - private void initUserListRecyclerView(){ - mUserRecyclerAdapter = new UserRecyclerAdapter(mUserList); - mUserListRecyclerView.setAdapter(mUserRecyclerAdapter); - mUserListRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + private void addMapMarkers() { + if (mGoogleMap != null) { + + if (mClusterManager == null) { + mClusterManager = new ClusterManager(getActivity().getApplicationContext(), mGoogleMap); + } + + for (UserLocation userLocation : mUserLocations) { + + Log.d(TAG, "addMapMarkers: location: " + userLocation.getGeo_point().toString()); + try { + /* + String snippet = ""; + if(userLocation.getUser().getUser_id().equals(FirebaseAuth.getInstance().getUid())){ + snippet = "This is you"; + } + else{ + snippet = "Determine route to " + userLocation.getUser().getUsername() + "?"; + } + + int avatar = R.drawable.cartman_cop; // set the default avatar + try{ + avatar = Integer.parseInt(userLocation.getUser().getAvatar()); + }catch (NumberFormatException e){ + Log.d(TAG, "addMapMarkers: no avatar for " + userLocation.getUser().getUsername() + ", setting default."); + }*/ + + String snippet = ""; + MyItem newClusterMarker = new MyItem( + userLocation.getGeo_point().getLatitude(), userLocation.getGeo_point().getLongitude(), + userLocation.getUser().getUsername(), + snippet + ); + + mClusterManager.addItem(newClusterMarker); + mClusterMarkers.add(newClusterMarker); + } catch (NullPointerException e) { + Log.e(TAG, "addMapMarkers: NullPointerException: " + e.getMessage()); + } + + } + mClusterManager.cluster(); + + setCameraView(); + } } -} + private void setCameraView() { + //Define top right corner and bottom left corner for boundary + //Overall map view window takes up area of 0.2 x 0.2 = 0.4 latLng + double bottomBoundary = mUserPosition.getGeo_point().getLatitude() - 0.1; + double leftBoundary = mUserPosition.getGeo_point().getLongitude() - 0.1; + double topBoundary = mUserPosition.getGeo_point().getLatitude() + 0.1; + double rightBoundary = mUserPosition.getGeo_point().getLongitude() + 0.1; + mMapBoundary = new LatLngBounds( + new LatLng(bottomBoundary, leftBoundary), + new LatLng(topBoundary, rightBoundary) + ); + mGoogleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(mMapBoundary, 0)); + } + private void setUserPosition() { + for (UserLocation userLocation : mUserLocations) { + if (userLocation.getUser().getUser_id().equals(FirebaseAuth.getInstance().getUid())) { + mUserPosition = userLocation; + } + } + } + private void initGoogleMap(Bundle savedInstanceState) { + // *** IMPORTANT *** + // MapView requires that the Bundle you pass contain _ONLY_ MapView SDK + // objects or sub-Bundles. + Bundle mapViewBundle = null; + if (savedInstanceState != null) { + mapViewBundle = savedInstanceState.getBundle(MAPVIEW_BUNDLE_KEY); + } + mMapView.onCreate(mapViewBundle); + mMapView.getMapAsync(this); + } + private void initUserListRecyclerView() { + mUserRecyclerAdapter = new UserRecyclerAdapter(mUserList); + mUserListRecyclerView.setAdapter(mUserRecyclerAdapter); + mUserListRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Bundle mapViewBundle = outState.getBundle(MAPVIEW_BUNDLE_KEY); + if (mapViewBundle == null) { + mapViewBundle = new Bundle(); + outState.putBundle(MAPVIEW_BUNDLE_KEY, mapViewBundle); + } + mMapView.onSaveInstanceState(mapViewBundle); + } + @Override + public void onResume() { + super.onResume(); + mMapView.onResume(); + } + + @Override + public void onStart() { + super.onStart(); + mMapView.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + mMapView.onStop(); + } + + @Override + public void onMapReady(GoogleMap map) { + map.addMarker(new MarkerOptions().position(new LatLng(0, 0)).title("Marker")); + if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(getActivity(), + Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + return; + } + map.setMyLocationEnabled(true); + mGoogleMap = map; + addMapMarkers(); + } + + @Override + public void onPause() { + mMapView.onPause(); + super.onPause(); + } + @Override + public void onDestroy() { + mMapView.onDestroy(); + super.onDestroy(); + } + @Override + public void onLowMemory() { + super.onLowMemory(); + mMapView.onLowMemory(); + } +} diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/util/MyClusterManagerRenderer.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/util/MyClusterManagerRenderer.java new file mode 100644 index 0000000..6b3f724 --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/util/MyClusterManagerRenderer.java @@ -0,0 +1,67 @@ +package com.codingwithmitch.googlemaps2018.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + + +import com.codingwithmitch.googlemaps2018.R; +import com.codingwithmitch.googlemaps2018.models.ClusterMarker; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.maps.android.clustering.Cluster; + +import com.google.maps.android.clustering.ClusterManager; +import com.google.maps.android.clustering.view.DefaultClusterRenderer; +import com.google.maps.android.ui.IconGenerator; + + +public class MyClusterManagerRenderer extends DefaultClusterRenderer +{ + + private final IconGenerator iconGenerator; + private final ImageView imageView; + private final int markerWidth; + private final int markerHeight; + + public MyClusterManagerRenderer(Context context, GoogleMap googleMap, + ClusterManager clusterManager) { + + super(context, googleMap, clusterManager); + + // initialize cluster item icon generator + iconGenerator = new IconGenerator(context.getApplicationContext()); + imageView = new ImageView(context.getApplicationContext()); + markerWidth = (int) context.getResources().getDimension(R.dimen.custom_marker_image); + markerHeight = (int) context.getResources().getDimension(R.dimen.custom_marker_image); + imageView.setLayoutParams(new ViewGroup.LayoutParams(markerWidth, markerHeight)); + int padding = (int) context.getResources().getDimension(R.dimen.custom_marker_padding); + imageView.setPadding(padding, padding, padding, padding); + iconGenerator.setContentView(imageView); + + } + + /** + * Rendering of the individual ClusterItems + * @param item + * @param markerOptions + */ + @Override + protected void onBeforeClusterItemRendered(ClusterMarker item, MarkerOptions markerOptions) { + + imageView.setImageResource(item.getIconPicture()); + Bitmap icon = iconGenerator.makeIcon(); + markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon)).title(item.getTitle()); + } + + + @Override + protected boolean shouldRenderAsCluster(Cluster cluster) { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_list.xml b/app/src/main/res/layout/fragment_user_list.xml index f94bc99..623dc14 100644 --- a/app/src/main/res/layout/fragment_user_list.xml +++ b/app/src/main/res/layout/fragment_user_list.xml @@ -1,17 +1,47 @@ - - - + android:orientation="vertical" + android:weightSum="100" + android:background="@color/White"> + + + + + + + + + + + - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..623d3a5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index a8204d7..f671cbc 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,9 @@ allprojects { repositories { google() jcenter() + maven{ + url "https://maven.google.com" + } } } From ba6d3c83e5458f980a8133425947f0bf763bda86 Mon Sep 17 00:00:00 2001 From: lamessk Date: Tue, 27 Nov 2018 22:45:05 -0700 Subject: [PATCH 4/8] -Map can determine routes and use maps to navigate there --- .idea/caches/build_file_checksums.ser | Bin 537 -> 537 bytes app/build.gradle | 3 + .../googlemaps2018/models/PolylineData.java | 39 +++ .../googlemaps2018/ui/UserListFragment.java | 273 +++++++++++++++++- .../main/res/layout/fragment_user_list.xml | 12 + 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/models/PolylineData.java diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index d6ff25bddd97daead506f251784077a78a9427c1..e779ea31f7902b6d9763a3f7296469ad7d7d73b0 100644 GIT binary patch delta 35 tcmV+;0NnqX1epYom;_i&23wJwcMuOA1);t7{1Nd+lptSbzA=*{0ru-u4gmlF delta 35 tcmV+;0NnqX1epYom;_gJgVm9ocM#No=|aJ4v@;bfdFJ->#&VM*0rn!h5k3F_ diff --git a/app/build.gradle b/app/build.gradle index ebbafde..00fd6a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,9 @@ dependencies { // Google marker cluster implementation'com.google.maps.android:android-maps-utils:0.5+' + //Google directions + implementation 'com.google.maps:google-maps-services:0.9.0' + implementation 'org.slf4j:slf4j-simple:1.7.25' //Google services implementation 'com.google.android.gms:play-services-maps:15.0.1' diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PolylineData.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PolylineData.java new file mode 100644 index 0000000..5f29dc5 --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PolylineData.java @@ -0,0 +1,39 @@ +package com.codingwithmitch.googlemaps2018.models; + +import com.google.android.gms.maps.model.Polyline; +import com.google.maps.model.DirectionsLeg; + +public class PolylineData { + + private Polyline polyline; + private DirectionsLeg leg; + + public PolylineData(Polyline polyline, DirectionsLeg leg) { + this.polyline = polyline; + this.leg = leg; + } + + public Polyline getPolyline() { + return polyline; + } + + public void setPolyline(Polyline polyline) { + this.polyline = polyline; + } + + public DirectionsLeg getLeg() { + return leg; + } + + public void setLeg(DirectionsLeg leg) { + this.leg = leg; + } + + @Override + public String toString() { + return "PolylineData{" + + "polyline=" + polyline + + ", leg=" + leg + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java index 0d00065..0f8d0fe 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java @@ -1,25 +1,34 @@ package com.codingwithmitch.googlemaps2018.ui; import android.Manifest; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Camera; +import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; 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.Toast; import com.codingwithmitch.googlemaps2018.R; import com.codingwithmitch.googlemaps2018.adapters.UserRecyclerAdapter; import com.codingwithmitch.googlemaps2018.models.ClusterMarker; import com.codingwithmitch.googlemaps2018.models.MyItem; +import com.codingwithmitch.googlemaps2018.models.PolylineData; import com.codingwithmitch.googlemaps2018.models.User; import com.codingwithmitch.googlemaps2018.models.UserLocation; import com.codingwithmitch.googlemaps2018.util.MyClusterManagerRenderer; @@ -29,16 +38,27 @@ import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; import com.google.firebase.auth.FirebaseAuth; +import com.google.maps.DirectionsApiRequest; +import com.google.maps.GeoApiContext; +import com.google.maps.PendingResult; import com.google.maps.android.clustering.ClusterManager; import com.google.maps.android.clustering.view.ClusterRenderer; +import com.google.maps.internal.PolylineEncoding; +import com.google.maps.model.DirectionsResult; +import com.google.maps.model.DirectionsRoute; import java.util.ArrayList; +import java.util.List; import static com.codingwithmitch.googlemaps2018.Constants.MAPVIEW_BUNDLE_KEY; -public class UserListFragment extends Fragment implements OnMapReadyCallback { +public class UserListFragment extends Fragment implements + OnMapReadyCallback, View.OnClickListener, GoogleMap.OnInfoWindowClickListener, GoogleMap.OnPolylineClickListener { private static final String TAG = "UserListFragment"; @@ -57,7 +77,10 @@ public class UserListFragment extends Fragment implements OnMapReadyCallback { private ClusterManager mClusterManager; private MyClusterManagerRenderer mClusterManagerRenderer; private ArrayList mClusterMarkers = new ArrayList<>(); - + private GeoApiContext mGeoApiContext = null; + private ArrayList mPolylinesData = new ArrayList<>(); + private Marker mSelectedMarker = null; + private ArrayList mTripMarkers = new ArrayList<>(); public static UserListFragment newInstance() { return new UserListFragment(); @@ -72,13 +95,61 @@ public void onCreate(@Nullable Bundle savedInstanceState) { } } + private void addPolylinesToMap(final DirectionsResult result){ + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Log.d(TAG, "run: result routes: " + result.routes.length); + if(mPolylinesData.size() > 0 ){ + for(PolylineData polylineData : mPolylinesData){ + polylineData.getPolyline().remove(); + } + mPolylinesData.clear(); + mPolylinesData = new ArrayList<>(); + } + + double duration = 9999; + for(DirectionsRoute route: result.routes){ + Log.d(TAG, "run: leg: " + route.legs[0].toString()); + List decodedPath = PolylineEncoding.decode(route.overviewPolyline.getEncodedPath()); + + List newDecodedPath = new ArrayList<>(); + + // This loops through all the LatLng coordinates of ONE polyline. + for(com.google.maps.model.LatLng latLng: decodedPath){ + +// Log.d(TAG, "run: latlng: " + latLng.toString()); + + newDecodedPath.add(new LatLng( + latLng.lat, + latLng.lng + )); + } + Polyline polyline = mGoogleMap.addPolyline(new PolylineOptions().addAll(newDecodedPath)); + polyline.setColor(ContextCompat.getColor(getActivity(), R.color.darkGrey)); + polyline.setClickable(true); + mPolylinesData.add(new PolylineData(polyline, route.legs[0])); + + double tempDuration = route.legs[0].duration.inSeconds; + if(tempDuration < duration){ + duration = tempDuration; + onPolylineClick(polyline); + zoomRoute(polyline.getPoints()); + } + + mSelectedMarker.setVisible(false); + } + } + }); + } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_user_list, container, false); mUserListRecyclerView = view.findViewById(R.id.user_list_recycler_view); mMapView = (MapView) view.findViewById(R.id.user_list_map); - + view.findViewById(R.id.btn_reset_map).setOnClickListener(this); initUserListRecyclerView(); initGoogleMap(savedInstanceState); @@ -87,9 +158,82 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return view; } + private void removeTripMarker(){ + for(Marker marker : mTripMarkers){ + marker.remove(); + } + } + + private void resetSelectedMarker(){ + if(mSelectedMarker != null){ + mSelectedMarker.setVisible(true); + mSelectedMarker = null; + removeTripMarker(); + } + } + + private void calculateDirections(Marker marker){ + Log.d(TAG, "calculateDirections: calculating directions."); + + com.google.maps.model.LatLng destination = new com.google.maps.model.LatLng( + marker.getPosition().latitude, + marker.getPosition().longitude + ); + DirectionsApiRequest directions = new DirectionsApiRequest(mGeoApiContext); + + //For showing all possible routes instead of only most efficient + directions.alternatives(true); + directions.origin( + new com.google.maps.model.LatLng( + mUserPosition.getGeo_point().getLatitude(), + mUserPosition.getGeo_point().getLongitude() + ) + ); + Log.d(TAG, "calculateDirections: destination: " + destination.toString()); + directions.destination(destination).setCallback(new PendingResult.Callback() { + @Override + public void onResult(DirectionsResult result) { + Log.d(TAG, "calculateDirections: routes: " + result.routes[0].toString()); + Log.d(TAG, "calculateDirections: duration: " + result.routes[0].legs[0].duration); + Log.d(TAG, "calculateDirections: distance: " + result.routes[0].legs[0].distance); + Log.d(TAG, "calculateDirections: geocodedWayPoints: " + result.geocodedWaypoints[0].toString()); + + addPolylinesToMap(result); + } + + @Override + public void onFailure(Throwable e) { + Log.e(TAG, "calculateDirections: Failed to get directions: " + e.getMessage() ); + + } + }); + } + + private void resetMap(){ + if(mGoogleMap != null) { + mGoogleMap.clear(); + + if(mClusterManager != null){ + mClusterManager.clearItems(); + } + + if (mClusterMarkers.size() > 0) { + mClusterMarkers.clear(); + mClusterMarkers = new ArrayList<>(); + } + + if(mPolylinesData.size() > 0){ + mPolylinesData.clear(); + mPolylinesData = new ArrayList<>(); + } + } + } + private void addMapMarkers() { if (mGoogleMap != null) { + resetMap(); + if (mClusterManager == null) { mClusterManager = new ClusterManager(getActivity().getApplicationContext(), mGoogleMap); } @@ -174,6 +318,12 @@ private void initGoogleMap(Bundle savedInstanceState) { mMapView.onCreate(mapViewBundle); mMapView.getMapAsync(this); + + if(mGeoApiContext == null){ + mGeoApiContext = new GeoApiContext.Builder() + .apiKey(getString(R.string.maps_api_key)) + .build(); + } } @@ -227,6 +377,8 @@ public void onMapReady(GoogleMap map) { map.setMyLocationEnabled(true); mGoogleMap = map; addMapMarkers(); + mGoogleMap.setOnPolylineClickListener(this); + mGoogleMap.setOnInfoWindowClickListener(this); } @Override @@ -246,4 +398,119 @@ public void onLowMemory() { super.onLowMemory(); mMapView.onLowMemory(); } + + @Override + public void onClick(View view) { + switch(view.getId()){ + case R.id.btn_reset_map: { + addMapMarkers(); + break; + } + } + } + + @Override + public void onInfoWindowClick(final Marker marker) { + + if(marker.getSnippet().contains("Duration")){ + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage("Open Google Maps?") + .setCancelable(true) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + public void onClick(@SuppressWarnings("unused") final DialogInterface dialog, @SuppressWarnings("unused") final int id) { + String latitude = String.valueOf(marker.getPosition().latitude); + String longitude = String.valueOf(marker.getPosition().longitude); + Uri gmmIntentUri = Uri.parse("google.navigation:q=" + latitude + "," + longitude); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + mapIntent.setPackage("com.google.android.apps.maps"); + + try{ + if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(mapIntent); + } + }catch (NullPointerException e){ + Log.e(TAG, "onClick: NullPointerException: Couldn't open map." + e.getMessage() ); + Toast.makeText(getActivity(), "Couldn't open map", Toast.LENGTH_SHORT).show(); + } + + } + }) + .setNegativeButton("No", new DialogInterface.OnClickListener() { + public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) { + dialog.cancel(); + } + }); + final AlertDialog alert = builder.create(); + alert.show(); + } + else { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + String message = "Do you want to navigate to " + marker.getTitle() + "?"; + builder.setMessage(message) + .setCancelable(true) + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + public void onClick(@SuppressWarnings("unused") final DialogInterface dialog, @SuppressWarnings("unused") final int id) { + resetSelectedMarker(); + mSelectedMarker = marker; + calculateDirections(marker); + dialog.dismiss(); + } + }) + .setNegativeButton("No", new DialogInterface.OnClickListener() { + public void onClick(final DialogInterface dialog, @SuppressWarnings("unused") final int id) { + dialog.cancel(); + } + }); + final AlertDialog alert = builder.create(); + alert.show(); + } + + } + + @Override + public void onPolylineClick(Polyline polyline) { + + for(PolylineData polylineData: mPolylinesData){ + Log.d(TAG, "onPolylineClick: toString: " + polylineData.toString()); + if(polyline.getId().equals(polylineData.getPolyline().getId())){ + polylineData.getPolyline().setColor(ContextCompat.getColor(getActivity(), R.color.blue1)); + polylineData.getPolyline().setZIndex(1); + + LatLng endLocation = new LatLng( + polylineData.getLeg().endLocation.lat, + polylineData.getLeg().endLocation.lng + ); + + Marker marker = mGoogleMap.addMarker(new MarkerOptions() + .position(endLocation) + .title(polylineData.getLeg().endAddress.toString()) + .snippet("Duration: " + polylineData.getLeg().duration)); + + mTripMarkers.add(marker); + marker.showInfoWindow(); + } + else{ + polylineData.getPolyline().setColor(ContextCompat.getColor(getActivity(), R.color.darkGrey)); + polylineData.getPolyline().setZIndex(0); + } + } + } + + public void zoomRoute(List lstLatLngRoute) { + + if (mGoogleMap == null || lstLatLngRoute == null || lstLatLngRoute.isEmpty()) return; + + LatLngBounds.Builder boundsBuilder = new LatLngBounds.Builder(); + for (LatLng latLngPoint : lstLatLngRoute) + boundsBuilder.include(latLngPoint); + + int routePadding = 120; + LatLngBounds latLngBounds = boundsBuilder.build(); + + mGoogleMap.animateCamera( + CameraUpdateFactory.newLatLngBounds(latLngBounds, routePadding), + 600, + null + ); + } } diff --git a/app/src/main/res/layout/fragment_user_list.xml b/app/src/main/res/layout/fragment_user_list.xml index 623dc14..2970512 100644 --- a/app/src/main/res/layout/fragment_user_list.xml +++ b/app/src/main/res/layout/fragment_user_list.xml @@ -32,6 +32,18 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/user_list_map" /> + + From 86a29a97b5d7b907cffa3cd9a8393dca01314706 Mon Sep 17 00:00:00 2001 From: lamessk Date: Wed, 28 Nov 2018 15:46:42 -0700 Subject: [PATCH 5/8] -Map can determine routes and use maps to navigate there --- .../googlemaps2018/ui/UserListFragment.java | 69 +++++++++++++++++- app/src/main/res/drawable-hdpi/ic_magnify.png | Bin 0 -> 577 bytes app/src/main/res/drawable-mdpi/ic_magnify.png | Bin 0 -> 401 bytes .../main/res/drawable-xhdpi/ic_magnify.png | Bin 0 -> 819 bytes .../main/res/drawable-xxhdpi/ic_magnify.png | Bin 0 -> 1263 bytes app/src/main/res/drawable/white_border.xml | 13 ++++ .../main/res/layout/fragment_user_list.xml | 35 ++++++++- 7 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/ic_magnify.png create mode 100644 app/src/main/res/drawable-mdpi/ic_magnify.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_magnify.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_magnify.png create mode 100644 app/src/main/res/drawable/white_border.xml diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java index 0f8d0fe..0316ffc 100644 --- a/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/ui/UserListFragment.java @@ -6,6 +6,8 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Camera; +import android.location.Address; +import android.location.Geocoder; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -18,9 +20,13 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; import android.widget.Toast; @@ -43,6 +49,7 @@ import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.GeoPoint; import com.google.maps.DirectionsApiRequest; import com.google.maps.GeoApiContext; import com.google.maps.PendingResult; @@ -52,19 +59,24 @@ import com.google.maps.model.DirectionsResult; import com.google.maps.model.DirectionsRoute; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import static com.codingwithmitch.googlemaps2018.Constants.MAPVIEW_BUNDLE_KEY; public class UserListFragment extends Fragment implements - OnMapReadyCallback, View.OnClickListener, GoogleMap.OnInfoWindowClickListener, GoogleMap.OnPolylineClickListener { + OnMapReadyCallback, View.OnClickListener, + GoogleMap.OnInfoWindowClickListener, + GoogleMap.OnPolylineClickListener { private static final String TAG = "UserListFragment"; + private static final float DEFAULT_ZOOM = 15f; //widgets private RecyclerView mUserListRecyclerView; private MapView mMapView; + private EditText mSearchText; //vars @@ -93,6 +105,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { mUserList = getArguments().getParcelableArrayList(getString(R.string.intent_user_list)); mUserLocations = getArguments().getParcelableArrayList(getString(R.string.intent_user_locations)); } + } private void addPolylinesToMap(final DirectionsResult result){ @@ -150,6 +163,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mUserListRecyclerView = view.findViewById(R.id.user_list_recycler_view); mMapView = (MapView) view.findViewById(R.id.user_list_map); view.findViewById(R.id.btn_reset_map).setOnClickListener(this); + mSearchText = (EditText) view.findViewById(R.id.input_search); + initUserListRecyclerView(); initGoogleMap(savedInstanceState); @@ -319,6 +334,19 @@ private void initGoogleMap(Bundle savedInstanceState) { mMapView.getMapAsync(this); + mSearchText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if(actionId == EditorInfo.IME_ACTION_SEARCH + || actionId == EditorInfo.IME_ACTION_DONE + ||keyEvent.getAction() == KeyEvent.ACTION_DOWN + || keyEvent.getAction() == KeyEvent.KEYCODE_ENTER) { + geoLocate(); + } + return false; + } + }); + if(mGeoApiContext == null){ mGeoApiContext = new GeoApiContext.Builder() .apiKey(getString(R.string.maps_api_key)) @@ -326,6 +354,45 @@ private void initGoogleMap(Bundle savedInstanceState) { } } + private void geoLocate(){ + Log.d(TAG, "geoLocate: geolocating"); + + String searchString = mSearchText.getText().toString(); + + Geocoder geocoder = new Geocoder(getContext()); + List
list = new ArrayList<>(); + try{ + list = geocoder.getFromLocationName(searchString, 1); + }catch (IOException e){ + Log.e(TAG, "geoLocate: IOException: " + e.getMessage() ); + } + + if(list.size() > 0){ + Address address = list.get(0); + + Log.d(TAG, "geoLocate: found a location: " + address.toString()); + //Toast.makeText(this, address.toString(), Toast.LENGTH_SHORT).show(); + + //GeoPoint gp = new GeoPoint(address.getLatitude(), address.getLongitude()); + //mUserPosition.setGeo_point(gp); + //setCameraView(); + + moveCamera(new LatLng(address.getLatitude(), address.getLongitude()), + DEFAULT_ZOOM, + address.getAddressLine(0)); + } + } + + private void moveCamera(LatLng latLng, float zoom, String title){ + mGoogleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, zoom)); + + MarkerOptions options = new MarkerOptions() + .position(latLng) + .title(title); + mGoogleMap.addMarker(options); + + } + private void initUserListRecyclerView() { mUserRecyclerAdapter = new UserRecyclerAdapter(mUserList); diff --git a/app/src/main/res/drawable-hdpi/ic_magnify.png b/app/src/main/res/drawable-hdpi/ic_magnify.png new file mode 100644 index 0000000000000000000000000000000000000000..6bed245a01a12cc36c13bb4d55ad82cef85d1733 GIT binary patch literal 577 zcmV-H0>1r;P)}NKTq_r(MUqZ5=_IYk8R&3A@{8cpVDfohLBQ_he@Ig7`4YsyB=CU{*aY_) zCO*K39m#t&_WCUt_+v~3m;sMu+zV(IePHQovcuU>;P_UMAmfij8VHlg69$sc%MH>b zPM;_PS7gt7i;NQ?2KGcG-vYi>lg3TNsv+kgEuub*lU0kFjJHeTxve&^L?$%*9gmWD zeya_vlLcO)rR!Y<@ z^|VCWh;E-PQH3l-CQkAu#)x`AMinCqx9I_)D%Xd5hpIj{=$!IMs7a}H+tn-MP7w7eDBktNB+Fe9Q8Yl-!? zCk7JF@;Bn*Zz$&k6AG`QhrGkmN}QdM9e#fD>g9hcsEHvvd*y1-$*cAh80NQ3y&@HG zq6wq=7bb!_Cj8QbG*P-U;VEYm@gjcTIK~7^zELSt`O0zSrKF^!r0mfz{$%5-iwSk+ P00000NkvXXu0mjf=O4v6O=sY{}k<*G=*K7l~+ascrnsM@1I zEJ6(jlpz@?Mud7bBnQV+!vU+HhINym2NnvGso{V_P{Yzm&{GH1vxb@u$Rt5eJyg$X zYB*pSQUau<9-N>6sgNP2q|*c9(@;H8)Np_tQXw-7S}5THyhw@mAQ1CYt1QSxDqui` zjS?0$I!Km-a2Pd9LKYzILNfdm)FD+sTnofY(CP>fyBdgXsObn$377`02XIwJhoC-( z$_rc%@4>U(OjaWw@ vM+YboOAeGHK&7ewKsIQlM%(nGU{C`9_j6?~0Sp6800000NkvXXu0mjf@CB75 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_magnify.png b/app/src/main/res/drawable-xhdpi/ic_magnify.png new file mode 100644 index 0000000000000000000000000000000000000000..d5ca6ac96f07e99d53f70619f2e4779c0db9f1b3 GIT binary patch literal 819 zcmV-31I+x1P)HqMWMD8K~V5gC@T0MMUhs;h#)P3=#x|sDJWD#MIiwn zd?>a^y|lKWruGv5D8Io8EX$efZZaEA-1ET?^01jX=j@p?GiQ|&i9{liNF)-8Txl)n z0{6fu7z5wISMUjpfPT;lR{l?pH-bU%6Z{Fu1n2{s>e=x%U;zAz;JBY};2c;{k8eF& zh2vg|U>Izu7v~Rv$qy9&F+6{LTR9$3X@h23x^;un}wro!}vuDf9og zU>)Z*pR*XQtNkUgD#SRt@ZACPma!gjtqHd+2Iovoh^ix|2vSR@o162yK!GqeYf`*Y z?EzE7xSvC2Q_b|ivW4w%F!oWfNQ{4$i}SjW%@PLoI-KJ%G5$L)0XRt*H|F>_+ej?2 z1xFcHwFZd*+=^kISHwJLI0WDgVc4M<=D9-5li?76FFvD|Vmh&P5CM3~Apri9+qAj* z#&N{+HE2$Ll}17 z@f`Z{^MRP>1cv||v)uk%)`2u}94^GP)FNxO<<@rH;oBb}#%Hkulys9YP}YNae|wPf@V z*cM`({ouJJ?TZd;ODx(9J}P@tbIt^**R;I$f_-KuxvKs(Jr~OSf6=tV^s-|6(L}%H zoC??TUsmB-;zjeU_xBIb%rywQ4>%msxgc~#+yJME@uoNjA!Ur7tit)Mr-1JEWPv{j zY0h;)ACxYdWw8{(>3MU%==N=O4a)w}gmor(w>JG2d(2%y7sJ=4)%6VA1*Zab3jIZC zwoDM3R01SHFm)kc&&$Du+5$2morEB?1_Ys5f{>u)kWcz@Xpu~?Z;=IV4U}32L7yx; xRl-y{Clr$Cyf+h$E0IVf5{X12kw_Fpe*x%ul+yeZScd=r002ovPDHLkV1ipUcTfNT literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_magnify.png b/app/src/main/res/drawable-xxhdpi/ic_magnify.png new file mode 100644 index 0000000000000000000000000000000000000000..320c4129e163b377f4320e0fc92d54b977ff7fda GIT binary patch literal 1263 zcmV8RZK5vFx@?y-RZUGJ$PTTo0;l*)78~A)k_2b00000000000000000027KwsuG zW(o5s^Ct5lvz}SYtYThb?q(J-$1=0ww)Q)H5%U7GiTRWH%aT8s4b1(_Da_7rr#*6* z8<;ObI9$gT<~`Z;?*1D?nb$*xnPmBmS<37Nw+EQP zd=r7=TT=ud6lce0F_$pEI&gl1PnpBxDu6h@If?Lbhk25@owQB8&40!^3b&iRBFn z4FkRyaeZb81r^q|?2*l3Kc9BIS2J+RZ!$a-MZO4-tKxfG13m4?t{g*BaNd_NRG>*c zuha(t-eh>HcTHp8ULOR|y<{)W0Q+uf?Az^w05usr z#id_J_u^)Y4+2zb26)V|ZM~Q;HTJFcL4YHg0aiPB-K}4k?Ykc!?VH?4vaem-gJI!+$F6F6n4xy*^!o-E~H zolSGeBj_rkzf>w1<#d-;7u~yIZz`b)bR~16)TW50d`}A6)pfRPBM-5R%pGuxBB=eC z!l6`0^L!LQDvys0ga62!o~HYER6B1|?`SD`4Cx6qHRMWj)bbQ(Mk4-63+LCXtj$}< zQ+R;Pt%)L%R3h8kFe_`ySc{mS*c28}3?NTf71=e+N}Xm+5>KT#KA(A*?9LxZhPIVF z>jg4xf*Q!aeO$hKh&h{rCa>&P&XRD|U6l?bd{wCR#P<3U_Qp`mAPu;U6#i7pI|@qN z5?0Ppa6Y(N#qaij3)d3{dAm~)NRJ9<067oLn|Y8{tuOIPG-J^ z2*DZQQnEj`)E+)pYe`}asW5y$HX@K(2yak&&I^mUJD7c1-Lt~te?^3xxQHN6=2QyK zY&ECA@zR&NliEJ`amMmG58^Zv5~-!E4<%|=d=BAsIg3!PYD+y0_mR}K*7Ca5aTDQB zF=R6~k%F@ZQaRm2!TV%A>$`N)ZK!gksAr~%7J>-{L z>L9}ZGZP-fDMdv%iW%}lW=9YqO(I|>ARGb1LS{!;nA4P^hRKdF6C8)5%M^njfpR5+ zDLaye + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_list.xml b/app/src/main/res/layout/fragment_user_list.xml index 2970512..3e48fca 100644 --- a/app/src/main/res/layout/fragment_user_list.xml +++ b/app/src/main/res/layout/fragment_user_list.xml @@ -32,6 +32,38 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/user_list_map" /> + + + + + + + + android:id="@+id/btn_reset_map" + /> From 4ab240450fd4811c16285c57527b0da2fdd2eb25 Mon Sep 17 00:00:00 2001 From: rish Date: Wed, 28 Nov 2018 17:32:04 -0700 Subject: [PATCH 6/8] added map after login --- .idea/caches/build_file_checksums.ser | Bin 537 -> 0 bytes .idea/misc.xml | 32 +-- .../PlaceAutocompleteAdapter.java | 244 ++++++++++++++++++ .../googlemaps2018/models/PlaceInfo.java | 112 ++++++++ app/src/main/res/drawable-hdpi/ic_gps.png | Bin 0 -> 922 bytes app/src/main/res/drawable-hdpi/ic_info.png | Bin 0 -> 629 bytes app/src/main/res/drawable-hdpi/ic_menu.png | Bin 0 -> 159 bytes app/src/main/res/drawable-mdpi/ic_gps.png | Bin 0 -> 570 bytes app/src/main/res/drawable-mdpi/ic_info.png | Bin 0 -> 419 bytes app/src/main/res/drawable-mdpi/ic_menu.png | Bin 0 -> 146 bytes app/src/main/res/drawable-xhdpi/ic_gps.png | Bin 0 -> 1117 bytes app/src/main/res/drawable-xhdpi/ic_info.png | Bin 0 -> 860 bytes app/src/main/res/drawable-xhdpi/ic_menu.png | Bin 0 -> 190 bytes app/src/main/res/drawable-xxhdpi/ic_gps.png | Bin 0 -> 2011 bytes app/src/main/res/drawable-xxhdpi/ic_info.png | Bin 0 -> 1359 bytes app/src/main/res/drawable-xxhdpi/ic_menu.png | Bin 0 -> 225 bytes app/src/main/res/drawable/logo.png | Bin 0 -> 10557 bytes app/src/main/res/layout/activity_main.xml | 96 ++++--- app/src/main/res/layout/activity_mainold.xml | 37 +++ app/src/main/res/layout/activity_map.xml | 71 +++++ app/src/main/res/layout/activity_settings.xml | 77 ++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 23 files changed, 611 insertions(+), 64 deletions(-) delete mode 100644 .idea/caches/build_file_checksums.ser create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/PlaceAutocompleteAdapter.java create mode 100644 app/src/main/java/com/codingwithmitch/googlemaps2018/models/PlaceInfo.java create mode 100644 app/src/main/res/drawable-hdpi/ic_gps.png create mode 100644 app/src/main/res/drawable-hdpi/ic_info.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu.png create mode 100644 app/src/main/res/drawable-mdpi/ic_gps.png create mode 100644 app/src/main/res/drawable-mdpi/ic_info.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_gps.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_info.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_gps.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_info.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_menu.png create mode 100644 app/src/main/res/drawable/logo.png create mode 100644 app/src/main/res/layout/activity_mainold.xml create mode 100644 app/src/main/res/layout/activity_map.xml create mode 100644 app/src/main/res/layout/activity_settings.xml diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index e779ea31f7902b6d9763a3f7296469ad7d7d73b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcmZ4UmVvdnh`~NNKUXg?FQq6yGexf?KR>5fFEb@IQ7^qHF(oHeub?PDD>b=9F91S2 zm1gFoxMk*~I%lLNXBU^|7Q2L-Ts|(GuF1r} zK_Ic9ASMaqbf~vVix`-qoz{F7|G~x)@_>nf0c16U0KXjT!oA=B2!3>%q7a|9&k*Ru z;?$Co%)Insn8reeItDh7+Icq<_SxKL&(lrNlL~h_ys(5p!`Uh(v^ce>IL0Rt=<67$ zmKa1x7Ni!HWTqCE!VS41+jFy@VH3;C2F@98emK>YFz}!_6Rt2{(ky(5cE_5t56sIq z%YXa^^ixiLa$*iBelSdZrYJP?=hdZ;k6*r~_ov3Vw}e3uj~hPsGOyZ`u+(ppZJP13 IvjRyK0IQX}hX4Qo diff --git a/.idea/misc.xml b/.idea/misc.xml index 99202cc..fefec9b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,34 +1,6 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/PlaceAutocompleteAdapter.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/PlaceAutocompleteAdapter.java new file mode 100644 index 0000000..184d6af --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/PlaceAutocompleteAdapter.java @@ -0,0 +1,244 @@ +package com.codingwithmitch.googlemaps2018; +/* +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataBufferUtils; +import com.google.android.gms.location.places.AutocompleteFilter; +import com.google.android.gms.location.places.AutocompletePrediction; +import com.google.android.gms.location.places.AutocompletePredictionBuffer; +import com.google.android.gms.location.places.Places; +import com.google.android.gms.maps.model.LatLngBounds; + + +import android.content.Context; +import android.graphics.Typeface; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +/** + * Adapter that handles Autocomplete requests from the Places Geo Data API. + * {@link AutocompletePrediction} results from the API are frozen and stored directly in this + * adapter. (See {@link AutocompletePrediction#freeze()}.) + *

+ * Note that this adapter requires a valid {@link com.google.android.gms.common.api.GoogleApiClient}. + * The API client must be maintained in the encapsulating Activity, including all lifecycle and + * connection states. The API client must be connected with the {@link Places#GEO_DATA_API} API. + */ +public class PlaceAutocompleteAdapter + extends ArrayAdapter implements Filterable { + + private static final String TAG = "PlaceAutoCompleteAd"; + private static final CharacterStyle STYLE_BOLD = new StyleSpan(Typeface.BOLD); + /** + * Current results returned by this adapter. + */ + private ArrayList mResultList; + + /** + * Handles autocomplete requests. + */ + private GoogleApiClient mGoogleApiClient; + + /** + * The bounds used for Places Geo Data autocomplete API requests. + */ + private LatLngBounds mBounds; + + /** + * The autocomplete filter used to restrict queries to a specific set of place types. + */ + private AutocompleteFilter mPlaceFilter; + + /** + * Initializes with a resource for text rows and autocomplete query bounds. + * + * @see android.widget.ArrayAdapter#ArrayAdapter(android.content.Context, int) + */ + public PlaceAutocompleteAdapter(Context context, GoogleApiClient googleApiClient, + LatLngBounds bounds, AutocompleteFilter filter) { + super(context, android.R.layout.simple_expandable_list_item_2, android.R.id.text1); + mGoogleApiClient = googleApiClient; + mBounds = bounds; + mPlaceFilter = filter; + } + + /** + * Sets the bounds for all subsequent queries. + */ + public void setBounds(LatLngBounds bounds) { + mBounds = bounds; + } + + /** + * Returns the number of results received in the last autocomplete query. + */ + @Override + public int getCount() { + return mResultList.size(); + } + + /** + * Returns an item from the last autocomplete query. + */ + @Override + public AutocompletePrediction getItem(int position) { + return mResultList.get(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row = super.getView(position, convertView, parent); + + // Sets the primary and secondary text for a row. + // Note that getPrimaryText() and getSecondaryText() return a CharSequence that may contain + // styling based on the given CharacterStyle. + + AutocompletePrediction item = getItem(position); + + TextView textView1 = (TextView) row.findViewById(android.R.id.text1); + TextView textView2 = (TextView) row.findViewById(android.R.id.text2); + textView1.setText(item.getPrimaryText(STYLE_BOLD)); + textView2.setText(item.getSecondaryText(STYLE_BOLD)); + + return row; + } + + /** + * Returns the filter for the current set of autocomplete results. + */ + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + FilterResults results = new FilterResults(); + + // We need a separate list to store the results, since + // this is run asynchronously. + ArrayList filterData = new ArrayList<>(); + + // Skip the autocomplete query if no constraints are given. + if (constraint != null) { + // Query the autocomplete API for the (constraint) search string. + filterData = getAutocomplete(constraint); + } + + results.values = filterData; + if (filterData != null) { + results.count = filterData.size(); + } else { + results.count = 0; + } + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + + if (results != null && results.count > 0) { + // The API returned at least one result, update the data. + mResultList = (ArrayList) results.values; + notifyDataSetChanged(); + } else { + // The API did not return any results, invalidate the data set. + notifyDataSetInvalidated(); + } + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + // Override this method to display a readable result in the AutocompleteTextView + // when clicked. + if (resultValue instanceof AutocompletePrediction) { + return ((AutocompletePrediction) resultValue).getFullText(null); + } else { + return super.convertResultToString(resultValue); + } + } + }; + } + + /** + * Submits an autocomplete query to the Places Geo Data Autocomplete API. + * Results are returned as frozen AutocompletePrediction objects, ready to be cached. + * objects to store the Place ID and description that the API returns. + * Returns an empty list if no results were found. + * Returns null if the API client is not available or the query did not complete + * successfully. + * This method MUST be called off the main UI thread, as it will block until data is returned + * from the API, which may include a network request. + * + * @param constraint Autocomplete query string + * @return Results from the autocomplete API or null if the query was not successful. + * @see Places#GEO_DATA_API#getAutocomplete(CharSequence) + * @see AutocompletePrediction#freeze() + */ + private ArrayList getAutocomplete(CharSequence constraint) { + if (mGoogleApiClient.isConnected()) { + Log.i(TAG, "Starting autocomplete query for: " + constraint); + + // Submit the query to the autocomplete API and retrieve a PendingResult that will + // contain the results when the query completes. + PendingResult results = + Places.GeoDataApi + .getAutocompletePredictions(mGoogleApiClient, constraint.toString(), + mBounds, mPlaceFilter); + + // This method should have been called off the main UI thread. Block and wait for at most 60s + // for a result from the API. + AutocompletePredictionBuffer autocompletePredictions = results + .await(60, TimeUnit.SECONDS); + + // Confirm that the query completed successfully, otherwise return null + final Status status = autocompletePredictions.getStatus(); + if (!status.isSuccess()) { + Toast.makeText(getContext(), "Error contacting API: " + status.toString(), + Toast.LENGTH_SHORT).show(); + Log.e(TAG, "Error getting autocomplete prediction API call: " + status.toString()); + autocompletePredictions.release(); + return null; + } + + Log.i(TAG, "Query completed. Received " + autocompletePredictions.getCount() + + " predictions."); + + // Freeze the results immutable representation that can be stored safely. + return DataBufferUtils.freezeAndClose(autocompletePredictions); + } + Log.e(TAG, "Google API client is not connected for autocomplete query."); + return null; + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PlaceInfo.java b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PlaceInfo.java new file mode 100644 index 0000000..52ee9c4 --- /dev/null +++ b/app/src/main/java/com/codingwithmitch/googlemaps2018/models/PlaceInfo.java @@ -0,0 +1,112 @@ +package com.codingwithmitch.googlemaps2018.models; + + +import android.net.Uri; + +import com.google.android.gms.maps.model.LatLng; + +public class PlaceInfo { + + private String name; + private String address; + private String phoneNumber; + private String id; + private Uri websiteUri; + private LatLng latlng; + private float rating; + private String attributions; + + public PlaceInfo(String name, String address, String phoneNumber, String id, Uri websiteUri, + LatLng latlng, float rating, String attributions) { + this.name = name; + this.address = address; + this.phoneNumber = phoneNumber; + this.id = id; + this.websiteUri = websiteUri; + this.latlng = latlng; + this.rating = rating; + this.attributions = attributions; + } + + public PlaceInfo() { + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Uri getWebsiteUri() { + return websiteUri; + } + + public void setWebsiteUri(Uri websiteUri) { + this.websiteUri = websiteUri; + } + + public LatLng getLatlng() { + return latlng; + } + + public void setLatlng(LatLng latlng) { + this.latlng = latlng; + } + + public float getRating() { + return rating; + } + + public void setRating(float rating) { + this.rating = rating; + } + + public String getAttributions() { + return attributions; + } + + public void setAttributions(String attributions) { + this.attributions = attributions; + } + + @Override + public String toString() { + return "PlaceInfo{" + + "name='" + name + '\'' + + ", address='" + address + '\'' + + ", phoneNumber='" + phoneNumber + '\'' + + ", id='" + id + '\'' + + ", websiteUri=" + websiteUri + + ", latlng=" + latlng + + ", rating=" + rating + + ", attributions='" + attributions + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_gps.png b/app/src/main/res/drawable-hdpi/ic_gps.png new file mode 100644 index 0000000000000000000000000000000000000000..7f8134994229a06b1bc325121a903eaa1b40aa34 GIT binary patch literal 922 zcmV;L17-Y)P)V8Mcqf)x5BLVYNL_zfgrN<^Z9ee@x;Xbo0_g7NPc zh?0j8(_G?*>Kshq7W-Pta2m1Ka~@hzcOuO9bM8 z2H+84k{AdG1O#{q1Kt7_0o#BxK-)r~1vmw40Tuvno{QmqfO$X>U|0;cgSqw)Fb8C(Z-PGTIi2$0m?+z{ssByCZA#+(UnCci=-DCP9J(biLQbQJzj4MNILN*~ zB_zhkzJ}Y{=_}dvw`A>i7bQeSa`Q)I?SdNuyF|${vo2-hHZphO-oz?dyQP|}tT-o1 zJ}}>{YZP*8jLvDY?v&UT74@(cOAlKkvETH=A?CWTH3*a$0!i`?TEw;r%|m8fuS+(1 zU}C?iq^d#Sj>!{)q@$y#IVDi<1p-w|{oW9`P=axx8w~;%iZ0Y9w%t<#VSER13uw2M zW;V-e5XdM#;xl4fOeHG)AWFV5>n0rb!`8$&&gfhrYq#CxfhD43-(k6JhpZ%#70KE~ zHw4}ZN{L}fDN%G_{DGt#A0;8Lu|T3;cOcH6AORd_kZ;=jBu)hRH-!jHNX6PBOEmk{{FT1XDH`cv`~_ zyS+^&VqM`7%@;{9H_-`^`FPg0W;L6c4lZ$c_9PO)WOt%4LZ?%=W1KrhLeD_LSUW9Y6;lT#)qw*UYD07*qoM6N<$f~|0yh5!Hn literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_info.png b/app/src/main/res/drawable-hdpi/ic_info.png new file mode 100644 index 0000000000000000000000000000000000000000..760e0fa81972b75a4b7d2515597805a31709b182 GIT binary patch literal 629 zcmV-*0*d{KP)9gH#V1w zRn(d(a2EXmQ6bhIB>%%2#V&CqU-vL^E%#1Dgz@DSpDEd0PPZN}n{CUfU^V%3j|$dG zVVmBa)HT<4IN^$zYgjRCv8K+6D~2!D#p$}q3&Xg%UCBp@A=81RjVGoPdu!W~Wjs;c z5TKxUGK@z71T>72KP8q-lT3)3hBv0MWusO+8=!4-7im2KcLE70=?UlvB%r0&_e>Zk zn4@Sn`CuG>tnH6tm{bv97gBJ?{&gP2nR_3%1uWop{2c+pFCn#vrveEW>s@lX0tsmA zodZ>Y1eD!wCEON}(G!p$J_QgU!;968mH+~p+wKFG0tm=#6L3h30xV!)#>u{By3|V= zo*Bl?o3&M(ItX9Hy9TGWN)Oh)sYo$iFS&M}q*ZsW7?9-Gu=Z`4yR`ras5p7KQ7itz z$K>m<_#Hpy)qGes6^`ULms8QB!j@s}yYk?dVlrus=)>A~H6$E%BgVvsaK)vNhY>~Z z)Uw&dF5$&yF_y29Kq6JN5mVf;WiW^D)PVz-g%=yx2*W6dgP0eu@IuBn*PMfI!_fK= P00000NkvXXu0mjf;y@c? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_menu.png b/app/src/main/res/drawable-hdpi/ic_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e3cbc01da6148c9b28ee083c0f69647fb7a7a5 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtG*1`DkcwMxZ*1gjFc5JK{O+O5 z5*QR=QOQ=QQJ=Uz^v8ta2Yer9wWLHcG{i)esIL7hd10}2=3NG6HlB#Zx|yqu#nb{8 zD`#GHO>1Dh|Luj(t#>`GoKwC(oUrkclJS8C&V6Ex4S{mA&MvO`b!w74&_)JNS3j3^ HP6%A_P)rPFF+1BWn{&>JTM{h;@by5E4Ph{ z(3gOq!gnV?nQWKkB@!_bcA+TkaRDaT4p%u*BVl}WkU0mE$eDvondqsg8rO*DVSNO>>jC@8+`6zE7$)2jDBh)u!|aapE%C(1vwz=$JkJ`cmMzZ07*qo IM6N<$f(C*5IRF3v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_info.png b/app/src/main/res/drawable-mdpi/ic_info.png new file mode 100644 index 0000000000000000000000000000000000000000..b127802f2a561c4b8df2db8feffece0ee7f59521 GIT binary patch literal 419 zcmV;U0bKrxP)Cu@Cloc zq&xeku>(KoAv5!(GdnwJwGPaVzz9siJ$NMB-xDz8Gv2KO<{$yt4iXAF^O5)jD6la| zTx3Jh$F|AOL2Qw@$S3GalV5_=L-Ieq!G+|_eJ0mM^i66W2S~1$qqZlOf?)3;A|C?3 zc~$bRu-k7n-#g*yC_SP|_;=b0$T%l*)&^@~kSB0Wo`t?iabk`GHeMA&UBFEP0w%hE zg++iD)_%*Yg@3LORu%zVnlR%n`hNtd1_b0vFl#`-)b$v;3K+Q#A{PNk+3NPNM**c5 zQg4r!QK#xgj0Az+czP7CW>Y^^w-20xuYlt>=F(GZ1Mm|ddCGOq$gVt#NV({WY~ZOJ zvEFb2to|Iy!juT^)ah!R)Usbr&aW}QmkuqO<@ev^+KbHDb~=;fS; uc@2u&PbNH8UR^XJ-i3&`?!qV9+NxYFDzzYjR0}>(e1YOZ3%U_k?Lrq7MQ|@_ z=`B94)nPlc(odX{Tn&g~slJiPrERsmVka9qc17-J#fMvj62Noj& zA_5`;A_DHJd=##S<6-xTEU?Gy_@y zUAG+MFU7gI(|i@S@o@zQ_X98pC!unfnjmZCJ*)0j?#6a}S>BV~xic zNfIvWT!%qImCKjs3Qb`cG$lEJ8Ocs~6{N$XJpBnKF}9=u@Y}^FSa*y()NK(890XL; zR}h)xO?0ptB70rFe2@)|3YVSGZEMkdG*oovt3z?yD2#n?pzvw&Nb;2_ zXV^$6Rw5b4ladPq1e_`Ogcvc_NK!UVI6s+uF}pYfV-M&5wS^`~tp`7FNZ3@SbJj&@lPEJ}fq;I=nq3-m)%Yc#QKHPe z1p;OyYqq2k0Sgugn3b&A93%m8i88|>9Dz>11XN@9 z)4#{d#Wt16Feoc-mi>l7N+typ?<$O2GDyI@LL!u~RTjS?lq!~tD>9S}ontBOCZ!&a zjgk}+lYjD2M(aSOtQ{nZp*&L+;f7#=9c+?%?Ifu{WeaEG9fWi2n#q2^M?%4O8Vwbl z<8Xncv}&_OG)IR?0&H$LJOvF0qPK?eAw3Uq8vrUd;TLX+r5y4`eoB z++IKrKZFHTj^9I7R27YDTk>zVuY_^3v7P?0RmC#d%bT$ie# zosg{j+{HX0!{?D}#U`xV1Bo~bNjjPQiOdeB%{tq&1pAJAO#YUzjfE`rF6UCTD;BYz z5XUA=BN{P)iPFB}T*)xDy_;j)X2Cs^+X~X*>73U-Mn7CI0lw3Eg9vqgGsbDdM4Vz2 z$Y*IJxmZPRcr==O_jRl!b6A>7A$FD~vB!8qBr^OOZX6wB0lUKz{l`c|Ktw=9z`Z7* j@>Uvrg9;W&B!SC66!5P)jRW2o00000NkvXXu0mjf2!P}m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_info.png b/app/src/main/res/drawable-xhdpi/ic_info.png new file mode 100644 index 0000000000000000000000000000000000000000..6514a0cf26cfd0496a896ce2d58797518383f071 GIT binary patch literal 860 zcmV-i1Ec(jP)3Tc9jZxEE&fCb+Fqcm~XYONqk0T!5kq@%>;I)D?KQ$EJ&=q{@H>4OO_&3%;b-fa9?7 z+C<3AE(us&@eQ1K_%5O5sD||kuTyaDhq25v;LrjC_IU>Uw!nZh-f+{kz<^57<;$eR z#-~bVw^EhZAQk)!cq-`uT)IQZddN#OVam^d1&Obqsyd*_jGqB7Eiz!q&wy3i4Oq0u zfLT8SiWV8*n*<+AItEo65O@sAdH~f11pWR0QL?~*b6ziSWPt$(JOkD&FkppeKtaMl zM5O`OH1Xz+#747GbB{62*S>_puLaZ_YK znPw1?;+?>05bHahcz7o8a&)Vc+u=b&c7npVq*4)j-U6J`=+cibZoBCw7EndlX^%(b7@oMkam_P=y#Po4~-FGWX*0i05x4YeZI?GbkkYeoI>~AL^uEd literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_gps.png b/app/src/main/res/drawable-xxhdpi/ic_gps.png new file mode 100644 index 0000000000000000000000000000000000000000..713d79f3751d05ccc596f393b0018ac1fcc13730 GIT binary patch literal 2011 zcmV<12PF83P)rYfy5Fe>L<)KJtl#Nd=z>t{Elc{silidLIJFYG5XMeLI zs0#7Gvf%#@cMw~WS@2niF2sBYh5w<`?nuVBp%gLbq1kwgm}RFr&7bG^Z(e-I&xlB} z2)`Eb+&~zMh#^Eb;t;P_j@ZN@YcYSOn79X^o?UDc9~dB1H&DQ*|l z@+r(a=!4^=2qv46>cjd+B|_8@u-s2LA@P*-!cxb}lt{=mTu~iBJmj!<1?)IuW0F z^nGcZCy285R+};OxPZaeu+)b&p$%M!n^wqNrw|voSZA87i85yDtXXMqej)L6d~N95 z&nd*`vBp$R8am7K61Ag-poLGxt)B!;JwxPau1{~xE%2_<(q~X-Dv)>xu0g=l7GxIw z5-@dKbL~pD?Ou1g$YGXi^3*O7$^{KRhXrdk1)+dWk4v@bpw}=r#gspa+yj09I^CWT#KQSsY3CpNhu_|jt0qYbM@TWxuj43LhDC`PQH9?sn zFhOa_WP)ayNfjr2*P;RjKyBues;O`NR6ss=cxK{YK@f&+Rg5eySyVumBCoZAFmyp6 zyAhGeOJOCfkAnfhruHaqs==ZHY7}`rB+;0OC%b$v8aG`6CcaTzkNm(J3&NiU^=XQ3 z!J6$s)PkBpJg}rn-&;^Xw+5d>f;D#qp@6M|a`YTO)hbkG7~ZMk1-DgIC|9B!y*UU4 zwX)c0?ujh*C<%?DFisG*e_t_2T(uKLyG++0O8-#Xc&>>ye?4*4UF0Z z%up^-ps{Ym=h6gazBv7f%YctlARbNJ2^J_F z#3OstV1D$pjPnH5W2G0_RGLtUMDdMG+iI{xXC#Di8N8Vu&g8PiB`)AS;ZpWEf2PhF zwKsQwH}g5ETv`zA?-*F5k3oDab%D*5mz2T^$zx>rV1rvgd_aR5I@T`k?uKib-0K$)e++p(v8YLIss0QNq38J{KnV_zKaCJ3pRfhw@3>pq=?|hI>SYM#o}lGKX0R9JudAVYe{G_KaeF~+gm0S57C{_(ro4ep zZ@yOGkFxN_1PY`7O^VtSmGF)Wcx@aht@44Fiu<%O==NpoWVFHv1+BPU}t{51h`w=u?Zs`RAOJr8uj$Odpf;-efnQQS{M zPtmK&;zdyu_tQCMI(|wg`l)80?ZoIExgTHz=%002ovPDHLkV1lAMo?ZX| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_info.png b/app/src/main/res/drawable-xxhdpi/ic_info.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e45febb988dc2bdf6868557b11a789d02c3f0b GIT binary patch literal 1359 zcmV-V1+e;wP)yOiPZ>+5j7y-V7$obo>1D836t^0uoK8KZF zgsoFRJvZkc1?EER_O~YIVRsn{z8Nt|1a}?eiQJS|2SeJ zztrQ%^Esi8CfuU1DZ-SO(Gji;uY^3>aT6U~_G+Dg`Sh>i^j+5pr)@jGiL!>`@X{-0 zYiSEitUVGZS0rUCfj0pB5(jG~@cK+UA7kA&3~ufl!>LK+JH4CH+5a(dZ(CNYt}@0Ge$RE6aic<(5_Z1zzMb0^dthfFZ5USEf`}K zVFXP8WZ13qF~O7(0jlPm+dNSp!+cK=1wbEmx|w{Z87^{yC;)QDsQ@h$Msd55X-asN zgJWrQMLBE;J})T+;~{B}69I0E(~L8z>wKKCUy8$wv!FCLi8M>K%L+eryC}|yv#imj zBfy9_${K?@0;I%IR@3Pf?U^{r8c%eZtvI)pzoeCJIbJiq{-sKhP=a9sE)1!Noh#ya zr4&R?1h7Vi26;)tZU1=@z#6Nx&MIjgiudl8pbLif~=CGc~&N2u9L8Y zp+1M9apnuLpbPL;CncR00j%*I1hB>}9RWr~0Ba2C2yj6Ju!dG)zE$zvdKzQ_ zHY$anJB#v*B7kwyx<&aIb#jRSX1U>{FX*BOV7xJ_*NFhf6|a2(K^Fk`2{buv&Yckf zjBZ_Zyo>(4lJ;~{?dfNf3&R3^Rs^UT%&pZbmo_N^_`6*dt2b*RfNMThD!}MW#z_&t zb?7;v4A?2)vk2grPZZ8qZ82KKak#0Q6daV}E|Y5=Qw9x_NXravYMrj592~kj@T}~E z%CKEhvT;K<>)Ed?7{;=5;$W?voYvk~)2euGU*)vuYE+i(tm1Jf5;t(}=~f;Xo`*Ir z^TB`lo)P8Y@%H@=i^z~11cmhf=*CT=W_rYk2P>SEBlZNnZ>BQ znPgVz-?@nzOP*~i0Iw{pR)}^QWm)C_3!O7mK0Fb2$M0qsPh$t(jPkY@HA{sPJe;;( ziEWNN>>$9dY;28RRAx7c;7G@#Zf=v{Em0ipPQ<98Fc!BMnB+UhM%5fzSLJXWEDXs6E-oYMqiVq#*<>~)y|a)q_C745{5ZTW={Jkge9)8qL7SBH#ce z?4%YPDf3=yahw^b>_Sqf?6g~#lp9`W{AXtQV8|+PfCY$t#5d@cMP$z0fL4W@%xrNQxE$ecLQ-fUHx3vIVCg! E0C(^b literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ad80c4c902b01f8e4ced69f8e58a84ad1dad18 GIT binary patch literal 10557 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 MNhOI=F~h+B1wAZL;Q#;t literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index dd2c7a0..ac0e132 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,37 +1,71 @@ - - - - - - - - - - \ No newline at end of file + android:id="@+id/map" + tools:context=".MapsActivity" + android:name="com.google.android.gms.maps.SupportMapFragment" /> + + + + + + + + + + + +