Repository: rtchagas/pingplacepicker Branch: master Commit: 3572f07005cb Files: 189 Total size: 235.9 KB Directory structure: gitextract_fxfpf1cx/ ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── rtchagas/ │ │ │ └── pingplacepicker/ │ │ │ ├── Config.kt │ │ │ ├── PingPlacePicker.kt │ │ │ ├── helper/ │ │ │ │ ├── PermissionsHelper.kt │ │ │ │ └── UrlSignerHelper.kt │ │ │ ├── inject/ │ │ │ │ ├── PingKoinComponent.kt │ │ │ │ ├── RepositoryModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── model/ │ │ │ │ ├── Geometry.kt │ │ │ │ ├── Location.kt │ │ │ │ ├── Photo.kt │ │ │ │ ├── SearchResult.kt │ │ │ │ └── SimplePlace.kt │ │ │ ├── repository/ │ │ │ │ ├── PlaceRepository.kt │ │ │ │ └── googlemaps/ │ │ │ │ ├── CustomPlace.kt │ │ │ │ ├── GoogleMapsAPI.kt │ │ │ │ ├── GoogleMapsRepository.kt │ │ │ │ └── PlaceFromCoordinates.kt │ │ │ ├── ui/ │ │ │ │ ├── UiExtensions.kt │ │ │ │ ├── UiUtils.kt │ │ │ │ ├── activity/ │ │ │ │ │ ├── BaseActivity.kt │ │ │ │ │ └── PlacePickerActivity.kt │ │ │ │ ├── adapter/ │ │ │ │ │ └── PlacePickerAdapter.kt │ │ │ │ └── fragment/ │ │ │ │ └── PlaceConfirmDialogFragment.kt │ │ │ └── viewmodel/ │ │ │ ├── BaseViewModel.kt │ │ │ ├── PlaceConfirmDialogViewModel.kt │ │ │ ├── PlacePickerViewModel.kt │ │ │ └── Resource.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── bg_button_round.xml │ │ │ ├── ic_arrow_back_black_24dp.xml │ │ │ ├── ic_close_black_24dp.xml │ │ │ ├── ic_crosshairs_gps_black_24dp.xml │ │ │ ├── ic_magnify_black_24dp.xml │ │ │ ├── ic_magnify_toolbar_menu_24dp.xml │ │ │ ├── ic_map_marker_black_24dp.xml │ │ │ ├── ic_map_marker_radius_black_24dp.xml │ │ │ ├── ic_map_marker_select_red_48dp.xml │ │ │ ├── ic_map_marker_solid_red_32dp.xml │ │ │ ├── ic_places_accounting.xml │ │ │ ├── ic_places_airport.xml │ │ │ ├── ic_places_amusement_park.xml │ │ │ ├── ic_places_aquarium.xml │ │ │ ├── ic_places_art_gallery.xml │ │ │ ├── ic_places_atm.xml │ │ │ ├── ic_places_bakery.xml │ │ │ ├── ic_places_bank.xml │ │ │ ├── ic_places_bar.xml │ │ │ ├── ic_places_beauty_salon.xml │ │ │ ├── ic_places_bicycle_store.xml │ │ │ ├── ic_places_book_store.xml │ │ │ ├── ic_places_bowling_alley.xml │ │ │ ├── ic_places_bus_station.xml │ │ │ ├── ic_places_cafe.xml │ │ │ ├── ic_places_campground.xml │ │ │ ├── ic_places_car_dealer.xml │ │ │ ├── ic_places_car_rental.xml │ │ │ ├── ic_places_car_repair.xml │ │ │ ├── ic_places_car_wash.xml │ │ │ ├── ic_places_casino.xml │ │ │ ├── ic_places_cemetery.xml │ │ │ ├── ic_places_church.xml │ │ │ ├── ic_places_city_hall.xml │ │ │ ├── ic_places_clothing_store.xml │ │ │ ├── ic_places_convenience_store.xml │ │ │ ├── ic_places_courthouse.xml │ │ │ ├── ic_places_dentist.xml │ │ │ ├── ic_places_department_store.xml │ │ │ ├── ic_places_doctor.xml │ │ │ ├── ic_places_electrician.xml │ │ │ ├── ic_places_electronics_store.xml │ │ │ ├── ic_places_embassy.xml │ │ │ ├── ic_places_establishment.xml │ │ │ ├── ic_places_finance.xml │ │ │ ├── ic_places_fire_station.xml │ │ │ ├── ic_places_florist.xml │ │ │ ├── ic_places_food.xml │ │ │ ├── ic_places_funeral_home.xml │ │ │ ├── ic_places_furniture_store.xml │ │ │ ├── ic_places_gas_station.xml │ │ │ ├── ic_places_gym.xml │ │ │ ├── ic_places_hair_care.xml │ │ │ ├── ic_places_hardware_store.xml │ │ │ ├── ic_places_health.xml │ │ │ ├── ic_places_hindu_temple.xml │ │ │ ├── ic_places_home_goods_store.xml │ │ │ ├── ic_places_hospital.xml │ │ │ ├── ic_places_insurance_agency.xml │ │ │ ├── ic_places_jewelry_store.xml │ │ │ ├── ic_places_laundry.xml │ │ │ ├── ic_places_lawyer.xml │ │ │ ├── ic_places_library.xml │ │ │ ├── ic_places_liquor_store.xml │ │ │ ├── ic_places_local_government_office.xml │ │ │ ├── ic_places_locksmith.xml │ │ │ ├── ic_places_lodging.xml │ │ │ ├── ic_places_meal_takeaway.xml │ │ │ ├── ic_places_mosque.xml │ │ │ ├── ic_places_movie_rental.xml │ │ │ ├── ic_places_movie_theater.xml │ │ │ ├── ic_places_moving_company.xml │ │ │ ├── ic_places_museum.xml │ │ │ ├── ic_places_night_club.xml │ │ │ ├── ic_places_painter.xml │ │ │ ├── ic_places_park.xml │ │ │ ├── ic_places_parking.xml │ │ │ ├── ic_places_pet_store.xml │ │ │ ├── ic_places_pharmacy.xml │ │ │ ├── ic_places_physiotherapist.xml │ │ │ ├── ic_places_place_of_worship.xml │ │ │ ├── ic_places_plumber.xml │ │ │ ├── ic_places_police.xml │ │ │ ├── ic_places_post_office.xml │ │ │ ├── ic_places_real_estate_agency.xml │ │ │ ├── ic_places_restaurant.xml │ │ │ ├── ic_places_roofing_contractor.xml │ │ │ ├── ic_places_rv_park.xml │ │ │ ├── ic_places_school.xml │ │ │ ├── ic_places_shoe_store.xml │ │ │ ├── ic_places_shopping_mall.xml │ │ │ ├── ic_places_spa.xml │ │ │ ├── ic_places_stadium.xml │ │ │ ├── ic_places_storage.xml │ │ │ ├── ic_places_store.xml │ │ │ ├── ic_places_subway_station.xml │ │ │ ├── ic_places_supermarket.xml │ │ │ ├── ic_places_synagogue.xml │ │ │ ├── ic_places_taxi_stand.xml │ │ │ ├── ic_places_train_station.xml │ │ │ ├── ic_places_transit_station.xml │ │ │ ├── ic_places_travel_agency.xml │ │ │ ├── ic_places_veterinary_care.xml │ │ │ ├── ic_places_zoo.xml │ │ │ ├── ic_refresh_black_24dp.xml │ │ │ └── wrapper_places_powered_by_google.xml │ │ ├── drawable-night/ │ │ │ └── wrapper_places_powered_by_google.xml │ │ ├── layout/ │ │ │ ├── activity_place_picker.xml │ │ │ ├── fragment_dialog_place_confirm.xml │ │ │ ├── item_place.xml │ │ │ └── places_autocomplete_impl_fragment_overlay.xml │ │ ├── menu/ │ │ │ └── menu_place_picker.xml │ │ ├── raw/ │ │ │ └── maps_night_style.json │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── config.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-ar/ │ │ │ └── strings.xml │ │ ├── values-ca/ │ │ │ └── strings.xml │ │ ├── values-cs/ │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-es-rES/ │ │ │ └── strings.xml │ │ ├── values-ko-rKR/ │ │ │ └── strings.xml │ │ ├── values-pt/ │ │ │ └── strings.xml │ │ ├── values-tr/ │ │ │ └── strings.xml │ │ ├── values-v21/ │ │ │ └── styles.xml │ │ ├── values-w600dp/ │ │ │ └── config.xml │ │ └── values-zh-rTW/ │ │ └── strings.xml │ └── test/ │ └── java/ │ └── com/ │ └── rtchagas/ │ └── pingplacepicker/ │ └── ExampleUnitTest.java ├── sample/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── rtchagas/ │ │ │ └── pingsample/ │ │ │ └── MainActivity.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_launcher_background.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── keys.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── values-night/ │ │ └── colors.xml │ └── test/ │ └── java/ │ └── com/ │ └── rtchagas/ │ └── pingsample/ │ └── ExampleUnitTest.kt └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/ # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Others .DS_Store hidden_keys.xml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: README.md ================================================ # PING - Because Ping Is Not Google's Place Picker 😉 [![](https://jitpack.io/v/rtchagas/pingplacepicker.svg)](https://jitpack.io/#rtchagas/pingplacepicker) [![](https://img.shields.io/badge/MinSDK-19-blue)](#) If you're here looking for a place picker you have probably read this: ![Google Place Picker was deprecated](https://github.com/rtchagas/pingplacepicker/blob/master/images/google_picker_deprecated.png?raw=true) As of the end of January 2019, Google deprecated the so useful Place Picker bundled in the Places SDK for Android. The main reason was due the new pricing model of the [Places API](https://developers.google.com/places/android-sdk/usage-and-billing). **PING** Place Picker is here to help you to (almost) plug-and-play replace the original Google's Place Picker. Map expanded Place selected Results expanded Search result ## A key difference Different than Google's Place Picker, PING by default **doesn't** search for places according to where the user is pointing the map to. Instead, it shows only the nearby places in the **current** location. This was intentional and the reason is simple. By using the **/nearbysearch** from [Google Places Web API](https://developers.google.com/places/web-service/search#PlaceSearchRequests) we are going to be charged *a lot* for each map movement. ![NearbySearch warning](https://github.com/rtchagas/pingplacepicker/blob/master/images/nearby_search_warning.png?raw=true) According to [Nearby Search pricing](https://developers.google.com/maps/billing/understanding-cost-of-use#nearby-search) each request to the API is going to cost 0.04 USD per each (40.00 USD per 1000). To avoid the extra cost of **/nearbysearch**, PING relies on Place API's **findCurrentPlace()** that is going to cost 0.030 USD per each (30.00 USD per 1000). Moreover, we don't fire a new request when the user moves the map. ## Enabling nearby searches If you do want to fetch places from a custom location or refresh them when the user moves the map, you must enable /nearbysearch queries in PING. To do that, enable this flag in your project: ```xml true ``` By doing so, PING behaviour will be slightly changed: - All places will be fetched by /nearbysearch queries. - You get a button to refresh the places for the current location. - You can set the initial map position to get the places from via `pingBuilder.setLatLng(LatLng)` ## Why use PING? PING is based entirely on Google Places and MAPs APIs. Google has the biggest places database available to us developers with most up to date and curated places information. It is worth to notice that Google provides US$ 200 (free) per month to be used with Places API. This should be more than enough for small applications that rely on Places data. ## Download Add Jitpack in your root build.gradle at the end of repositories: ```gradle allprojects { repositories { ... maven { url 'https://jitpack.io' } } } ``` Step 2. Add the dependency ```gradle dependencies { // Places library implementation 'com.google.android.libraries.places:places:2.0.0' // PING Place Picker implementation 'com.github.rtchagas:pingplacepicker:2.0.+' } ``` ## Setup 1. Add Google Play Services to your project - [How to](https://developers.google.com/android/guides/setup) 2. Sign up for API keys - [How to](https://developers.google.com/places/android-sdk/signup) 3. Add the Android API key to your **AndroidManifest** file as in the [sample project](https://github.com/rtchagas/pingplacepicker/blob/master/sample/src/main/AndroidManifest.xml#L15). 4. Optional but strongly recommended to enable R8 in you *[gradle.properties](https://github.com/rtchagas/pingplacepicker/blob/master/gradle.properties#L12)* file ## Hands on Check the [sample](https://github.com/rtchagas/pingplacepicker/tree/master/sample) project for a full working example. ### - Kotlin ```kotlin private fun showPlacePicker() { val builder = PingPlacePicker.IntentBuilder() builder.setAndroidApiKey("YOUR_ANDROID_API_KEY") .setMapsApiKey("YOUR_MAPS_API_KEY") // If you want to set a initial location rather then the current device location. // NOTE: enable_nearby_search MUST be true. // builder.setLatLng(LatLng(37.4219999, -122.0862462)) try { val placeIntent = pingBuilder.build(this) startActivityForResult(placeIntent, REQUEST_PLACE_PICKER) } catch (ex: Exception) { toast("Google Play Services is not Available") } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if ((requestCode == REQUEST_PLACE_PICKER) && (resultCode == Activity.RESULT_OK)) { val place: Place? = PingPlacePicker.getPlace(data!!) toast("You selected: ${place?.name}") } } ``` ### - Java ```java private void showPlacePicker() { PingPlacePicker.IntentBuilder builder = new PingPlacePicker.IntentBuilder(); builder.setAndroidApiKey("YOUR_ANDROID_API_KEY") .setMapsApiKey("YOUR_MAPS_API_KEY"); // If you want to set a initial location rather then the current device location. // NOTE: enable_nearby_search MUST be true. // builder.setLatLng(new LatLng(37.4219999, -122.0862462)) try { Intent placeIntent = builder.build(getActivity()); startActivityForResult(placeIntent, REQUEST_PLACE_PICKER); } catch (Exception ex) { // Google Play services is not available... } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if ((requestCode == REQUEST_PLACE_PICKER) && (resultCode == RESULT_OK)) { Place place = PingPlacePicker.getPlace(data); if (place != null) { Toast.makeText(this, "You selected the place: " + place.getName(), Toast.LENGTH_SHORT).show(); } } } ``` ## API Keys PING needs two API keys in order to work. It was decided to split the API keys to clearly distinguish what you're going to be charged for. Also, the Places Web API and the Geocoding API don't allow an Android API key to be used. To not expose an unrestricted key for all APIs, the Maps API key is now required. | Key | Restriction | Purpose |--|--|--| | Android key | [Android Applications](https://developers.google.com/places/android-sdk/signup#restrict-key) | Used as the Places API key. Main purpose is to retrieve the current places and place details. | Maps key | [APIs: Geocoding, Maps Static and Places API only](https://cloud.google.com/docs/authentication/api-keys#api_key_restrictions) | Used to fetch static maps, nearby places through Places Web API and perform reverse geocoding on the current user position. That is, discover the address that the user is current pointing to. Your key should look [like this](https://raw.githubusercontent.com/rtchagas/pingplacepicker/master/images/maps_api_key.png). **TIP:** It is strongly recommended to **not expose** your Maps API key in your resource files. Anyone could decompile your apk and have access to that key. To avoid this, the key should be at least obfuscated. A nice approach is to save the key in the cloud through "Firebase remote config" and fetch it at runtime. ## Configuration As some features are charged by Google, you can alter the default **PING** Place Picker behaviour by overriding below resources: ```xml true true false ``` ## Theming PING is fully customizable and you just need to override some colors to make it seamlessly connected to your app. Since release [2.0.0](https://github.com/rtchagas/pingplacepicker/releases/tag/2.0.0) PING supports dark/night mode by default.
Please make sure your app provide the correct resources to switch to night mode. You can always refer to [Material Design documentation](https://material.io/develop/android/theming/dark) to know more about dark theme and how to implement it. To customize PING you need to override these colors: For day/light theme: - `res/values/colors.xml` ```xml @color/material_teal500 @color/material_teal800 @color/material_white @color/material_deeporange500 @color/material_deeporange800 @color/material_white @color/material_grey200 @color/material_black @color/material_white @color/material_black @color/material_on_surface_emphasis_high_type @color/material_on_surface_emphasis_medium @color/material_deeporange400 @color/material_white ``` For night/dark theme: - `res/values-night/colors.xml` ```xml @color/material_teal300 @color/colorSurface @color/material_black @color/material_deeporange200 @color/material_deeporange300 @color/material_black @color/colorSurface @color/colorOnSurface #202125 @color/material_white @color/material_on_surface_emphasis_high_type @color/material_on_surface_emphasis_medium @color/material_deeporange200 @color/colorSurface ``` In case of doubt in how to implement the new styles, please take a look at the [sample app](https://github.com/rtchagas/pingplacepicker/tree/master/sample). ## Contribute Let's together make PING awesome! Please feel free to contribute with improvements. ## License Copyright 2020 Rafael Chagas 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. ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.1.1' classpath 'com.google.gms:google-services:4.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() maven { url "https://jitpack.io" } } } tasks.register('clean', Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Sun Nov 15 15:01:33 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip ================================================ FILE: gradle.properties ================================================ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx2512m android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jitpack.yml ================================================ jdk: openjdk17 ================================================ FILE: library/.gitignore ================================================ /build ================================================ FILE: library/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-parcelize' apply plugin: 'maven-publish' android { namespace = "com.rtchagas.pingplacepicker" compileSdk 33 defaultConfig { minSdkVersion 21 targetSdkVersion 33 vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } compileOptions { isCoreLibraryDesugaringEnabled() } kotlin { jvmToolchain(11) } kotlinOptions { freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } buildFeatures { viewBinding true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } publishing { singleVariant('release') { withSourcesJar() } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" // KTX implementation 'androidx.core:core-ktx:1.10.1' // Support library implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.material:material:1.9.0' // Android architecture components implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2' // Google Play Services implementation 'com.google.android.gms:play-services-location:21.0.1' implementation 'com.google.android.gms:play-services-maps:18.1.0' implementation 'com.google.maps.android:android-maps-utils:3.4.0' implementation 'com.google.android.libraries.places:places:3.2.0' // Koin for Android implementation 'io.insert-koin:koin-android:3.1.5' // Rx implementation 'io.reactivex.rxjava2:rxjava:2.2.21' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0' // Moshi implementation 'com.squareup.moshi:moshi:1.15.0' kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.15.0' // Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' // Coil implementation 'io.coil-kt:coil:2.4.0' // 3rd party implementation 'com.github.mcginty:material-colors:1.1.0' implementation 'com.karumi:dexter:6.2.3' testImplementation 'junit:junit:4.13.2' } afterEvaluate { publishing { publications { release(MavenPublication) { from components.release groupId = 'com.github.rtchagas' artifactId = 'pingplacepicker' } } } } ================================================ FILE: library/consumer-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile # Keep custom model classes -keep class com.rtchagas.pingplacepicker.model.** { *; } -dontnote com.rtchagas.pingplacepicker.model.** # Moshi # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** -keepclasseswithmembers class * { @com.squareup.moshi.* ; } -keep @com.squareup.moshi.JsonQualifier interface * # Enum field names are used by the integrated EnumJsonAdapter. # Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi. -keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum { ; } # The name of @JsonClass types is used to look up the generated adapter. -keepnames @com.squareup.moshi.JsonClass class * # Retain generated JsonAdapters if annotated type is retained. -if @com.squareup.moshi.JsonClass class * -keep class <1>JsonAdapter { (...); ; } -if @com.squareup.moshi.JsonClass class **$* -keep class <1>_<2>JsonAdapter { (...); ; } -keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl -keepclassmembers class kotlin.Metadata { public ; } # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* # Retrofit config # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and # EnclosingMethod is required to use InnerClasses. -keepattributes Signature, InnerClasses, EnclosingMethod # Retrofit does reflection on method and parameter annotations. -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations # Keep annotation default values (e.g., retrofit2.http.Field.encoded). -keepattributes AnnotationDefault # Retain service method parameters when optimizing. -keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; } # Ignore annotation used for build tooling. -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement # Ignore JSR 305 annotations for embedding nullability information. -dontwarn javax.annotation.** # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. -dontwarn kotlin.Unit # Top-level functions that can only be used by Kotlin. -dontwarn retrofit2.KotlinExtensions -dontwarn retrofit2.KotlinExtensions$* # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. -if interface * { @retrofit2.http.* ; } -keep,allowobfuscation interface <1> # Keep inherited services. -if interface * { @retrofit2.http.* ; } -keep,allowobfuscation interface * extends <1> # With R8 full mode generic signatures are stripped for classes that are not # kept. Suspend functions are wrapped in continuations where the type argument # is used. -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation # R8 full mode strips generic signatures from return types if not kept. -if interface * { @retrofit2.http.* public *** *(...); } -keep,allowoptimization,allowshrinking,allowobfuscation class <3> # With R8 full mode generic signatures are stripped for classes that are not kept. -keep,allowobfuscation,allowshrinking class retrofit2.Response ================================================ FILE: library/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: library/src/main/AndroidManifest.xml ================================================ ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/Config.kt ================================================ package com.rtchagas.pingplacepicker internal object Config { const val PLACE_IMG_WIDTH = 640 const val PLACE_IMG_HEIGHT = 320 const val STATIC_MAP_URL = "https://maps.googleapis.com/maps/api/staticmap?" + "size=${PLACE_IMG_WIDTH}x${PLACE_IMG_HEIGHT}" + "&markers=color:red|%.6f,%.6f" + "&key=%s" + "&language=%s" const val STATIC_MAP_URL_STYLE_DARK = "&style=element:geometry%7Ccolor:0x242f3e" + "&style=element:labels.text.fill%7Ccolor:0x746855" + "&style=element:labels.text.stroke%7Ccolor:0x242f3e" + "&style=feature:administrative.locality%7Celement:labels.text.fill%7Ccolor:0xd59563" + "&style=feature:poi%7Celement:labels.text.fill%7Ccolor:0xd59563" + "&style=feature:poi.park%7Celement:geometry%7Ccolor:0x263c3f" + "&style=feature:poi.park%7Celement:labels.text.fill%7Ccolor:0x6b9a76" + "&style=feature:road%7Celement:geometry%7Ccolor:0x38414e" + "&style=feature:road%7Celement:geometry.stroke%7Ccolor:0x212a37" + "&style=feature:road%7Celement:labels.text.fill%7Ccolor:0x9ca5b3" + "&style=feature:road.highway%7Celement:geometry%7Ccolor:0x746855" + "&style=feature:road.highway%7Celement:geometry.stroke%7Ccolor:0x1f2835" + "&style=feature:road.highway%7Celement:labels.text.fill%7Ccolor:0xf3d19c" + "&style=feature:transit%7Celement:geometry%7Ccolor:0x2f3948" + "&style=feature:transit.station%7Celement:labels.text.fill%7Ccolor:0xd59563" + "&style=feature:water%7Celement:geometry%7Ccolor:0x17263c" + "&style=feature:water%7Celement:labels.text.fill%7Ccolor:0x515c6d" + "&style=feature:water%7Celement:labels.text.stroke%7Ccolor:0x17263c" } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/PingPlacePicker.kt ================================================ package com.rtchagas.pingplacepicker import android.app.Activity import android.content.Intent import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.inject.PingKoinContext import com.rtchagas.pingplacepicker.ui.activity.PlacePickerActivity object PingPlacePicker { internal var androidApiKey: String = "" internal var mapsApiKey: String = "" internal var urlSigningSecret = "" internal var isNearbySearchEnabled = false internal var onPlaceSelectedListener: OnPlaceSelectedListener? = null class Builder { private val intent = Intent() /** * This key will be used to all nearby requests to Google Places API. */ fun setAndroidApiKey(androidKey: String): Builder { androidApiKey = androidKey return this } /** * This key will be used to nearby searches and reverse geocoding * requests to Google Maps HTTP API. */ fun setMapsApiKey(geoKey: String): Builder { mapsApiKey = geoKey return this } /** * The initial location that the map must be pointing to. * If this is set, PING will search for places near this location. */ fun setLatLng(location: LatLng): Builder { intent.putExtra(PlacePickerActivity.EXTRA_LOCATION, location) return this } /** * Sets the listener to be called when a place is selected. */ fun setOnPlaceSelectedListener(listener: OnPlaceSelectedListener): Builder { onPlaceSelectedListener = listener return this } /** * Enables URL signing for Google APIs that require it. * * Currently only Maps Statics API requires signing for some users. * * More info [here](https://developers.google.com/maps/documentation/maps-static/get-api-key#generating-digital-signatures) */ fun setUrlSigningSecret(secretKey: String): Builder { urlSigningSecret = secretKey return this } /** * Set whether the library should return the place coordinate retrieved from GooglePlace * or the actual selected location from google map */ fun setShouldReturnActualLatLng(shouldReturnActualLatLng: Boolean): Builder { intent.putExtra( PlacePickerActivity.EXTRA_RETURN_ACTUAL_LATLNG, shouldReturnActualLatLng ) return this } @Throws(GooglePlayServicesNotAvailableException::class) fun build(activity: Activity): Intent { PingKoinContext.init(activity.application) val result: Int = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) if (ConnectionResult.SUCCESS != result) { throw GooglePlayServicesNotAvailableException(result) } isNearbySearchEnabled = activity.resources.getBoolean(R.bool.enable_nearby_search) intent.setClass(activity, PlacePickerActivity::class.java) return intent } } /** * Listener to be called when PING returns a selected place. */ interface OnPlaceSelectedListener { /** * Called when PING returns a place selected by the user. * @param place the selected place. * @param latLng the selected latitude/longitude in the map. */ fun onPlaceSelected(place: Place, latLng: LatLng) } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/helper/PermissionsHelper.kt ================================================ package com.rtchagas.pingplacepicker.helper import android.Manifest import android.app.Activity import com.karumi.dexter.Dexter import com.karumi.dexter.listener.single.BasePermissionListener import com.karumi.dexter.listener.single.CompositePermissionListener import com.karumi.dexter.listener.single.DialogOnDeniedPermissionListener import com.rtchagas.pingplacepicker.R internal object PermissionsHelper { fun checkForLocationPermission(activity: Activity, listener: BasePermissionListener?) { val dialogPermissionListener = DialogOnDeniedPermissionListener.Builder .withContext(activity) .withTitle(R.string.permission_fine_location_title) .withMessage(R.string.permission_fine_location_message) .withButtonText(android.R.string.ok) .withIcon(R.drawable.ic_map_marker_radius_black_24dp) .build() val compositeListener = if (listener != null) { CompositePermissionListener(dialogPermissionListener, listener) } else { CompositePermissionListener(dialogPermissionListener) } Dexter.withContext(activity) .withPermission(Manifest.permission.ACCESS_FINE_LOCATION) .withListener(compositeListener) .check() } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/helper/UrlSignerHelper.kt ================================================ package com.rtchagas.pingplacepicker.helper import android.util.Base64 import android.util.Log import java.net.MalformedURLException import java.net.URI import java.net.URL import java.security.InvalidKeyException import java.security.NoSuchAlgorithmException import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec internal object UrlSignerHelper { private const val TAG = "UrlSignerHelper" fun signUrl(inputUrl: String, inputKey: String): String { // Convert the string to a URL so we can parse it var url: URL try { url = URL(inputUrl) } catch (e: MalformedURLException) { Log.e(TAG, "Could not parse the input URL") return inputUrl } // Encode the URL val uri = URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref) url = uri.toURL() // Convert the key from 'web safe' base 64 to binary val byteKey: ByteArray = Base64.decode(inputKey, Base64.URL_SAFE) return try { val signature = signRequest(url.path, url.query, byteKey) "$inputUrl&signature=$signature" } catch (ex: Exception) { Log.e(TAG, "Could not sign the URL. ${ex.message}") inputUrl } } @Throws(NoSuchAlgorithmException::class, InvalidKeyException::class) private fun signRequest(path: String, query: String, key: ByteArray): String { // Retrieve the proper URL components to sign val resource = "$path?$query" // Get an HMAC-SHA1 signing key from the raw key bytes val sha1Key = SecretKeySpec(key, "HmacSHA1") // Get an HMAC-SHA1 Mac instance and initialize it with the HMAC-SHA1 key val mac = Mac.getInstance("HmacSHA1") mac.init(sha1Key) // compute the binary signature for the request val sigBytes = mac.doFinal(resource.toByteArray()) // base 64 encode the binary signature return Base64.encodeToString(sigBytes, Base64.URL_SAFE or Base64.NO_WRAP) } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/inject/PingKoinComponent.kt ================================================ package com.rtchagas.pingplacepicker.inject import android.content.Context import com.rtchagas.pingplacepicker.BuildConfig import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.Koin import org.koin.core.component.KoinComponent import org.koin.core.logger.Level import org.koin.dsl.koinApplication internal object PingKoinContext { private lateinit var appContext: Context val koin: Koin by lazy { koinApplication { androidLogger(if (BuildConfig.DEBUG) Level.ERROR else Level.NONE) androidContext(appContext) modules(listOf(repositoryModule, viewModelModule)) }.koin } /** * Initializes the Dependency Injection framework by passing * the current application context. */ @Synchronized fun init(context: Context) { appContext = context.applicationContext } } internal interface PingKoinComponent : KoinComponent { override fun getKoin(): Koin = PingKoinContext.koin } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/inject/RepositoryModule.kt ================================================ package com.rtchagas.pingplacepicker.inject import com.google.android.libraries.places.api.Places import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.repository.PlaceRepository import com.rtchagas.pingplacepicker.repository.googlemaps.GoogleMapsAPI import com.rtchagas.pingplacepicker.repository.googlemaps.GoogleMapsRepository import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext import org.koin.dsl.bind import org.koin.dsl.module import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.moshi.MoshiConverterFactory internal val repositoryModule = module { // PlacesClient single { Places.initialize(androidContext(), PingPlacePicker.androidApiKey) return@single Places.createClient(androidContext()) } // GoogleMapsAPI single(createdAtStart = true) { val interceptor = HttpLoggingInterceptor() interceptor.level = HttpLoggingInterceptor.Level.NONE val client = OkHttpClient.Builder().addInterceptor(interceptor).build() val retrofit = Retrofit.Builder() .baseUrl("https://maps.googleapis.com/maps/api/") .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(client) .build() return@single retrofit.create(GoogleMapsAPI::class.java) } // GoogleMapsRepository single { GoogleMapsRepository(googleClient = get(), googleMapsAPI = get()) } bind PlaceRepository::class } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/inject/ViewModelModule.kt ================================================ package com.rtchagas.pingplacepicker.inject import com.rtchagas.pingplacepicker.viewmodel.PlaceConfirmDialogViewModel import com.rtchagas.pingplacepicker.viewmodel.PlacePickerViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module internal val viewModelModule = module { viewModel { PlacePickerViewModel(get()) } viewModel { PlaceConfirmDialogViewModel(get()) } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/model/Geometry.kt ================================================ package com.rtchagas.pingplacepicker.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class Geometry( @Json(name = "location") val location: Location ) ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/model/Location.kt ================================================ package com.rtchagas.pingplacepicker.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class Location( @Json(name = "lat") val lat: Double, @Json(name = "lng") val lng: Double ) ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/model/Photo.kt ================================================ package com.rtchagas.pingplacepicker.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class Photo( @Json(name = "height") val height: Int, @Json(name = "html_attributions") val htmlAttributions: List, @Json(name = "photo_reference") val photoReference: String, @Json(name = "width") val width: Int ) ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/model/SearchResult.kt ================================================ package com.rtchagas.pingplacepicker.model import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SearchResult( val results: List, val status: String ) ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/model/SimplePlace.kt ================================================ package com.rtchagas.pingplacepicker.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SimplePlace( @Json(name = "geometry") val geometry: Geometry, @Json(name = "name") val name: String = "", @Json(name = "photos") val photos: List = emptyList(), @Json(name = "place_id") val placeId: String, @Json(name = "types") val types: List = emptyList(), @Json(name = "vicinity") val vicinity: String = "", @Json(name = "formatted_address") val formattedAddress: String = "" ) ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/repository/PlaceRepository.kt ================================================ package com.rtchagas.pingplacepicker.repository import android.graphics.Bitmap import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.PhotoMetadata import com.google.android.libraries.places.api.model.Place import io.reactivex.Single /** * We decided to interface the Places repository as there's a lot of * room to improve the place search and retrieval. * We could have different repositories to fetch places locally from * a cached database or from other providers than Google. */ internal interface PlaceRepository { fun getNearbyPlaces(): Single> fun getNearbyPlaces(location: LatLng): Single> fun getPlacePhoto(photoMetadata: PhotoMetadata): Single fun getPlaceByLocation(location: LatLng): Single } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/repository/googlemaps/CustomPlace.kt ================================================ package com.rtchagas.pingplacepicker.repository.googlemaps import android.net.Uri import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.libraries.places.api.model.* import kotlinx.parcelize.Parcelize import com.google.android.libraries.places.api.model.Place.BooleanPlaceAttributeValue.UNKNOWN @Parcelize internal class CustomPlace( var placeId: String, var placeName: String, var placePhotos: MutableList, var placeAddress: String, var placeTypes: MutableList, var placeLatLng: LatLng ) : Place() { override fun getUserRatingsTotal(): Int? { return null } /** * Default value only. * Clients shouldn't rely on this. */ override fun getBusinessStatus(): BusinessStatus { return BusinessStatus.OPERATIONAL } override fun getName(): String { return placeName } override fun getOpeningHours(): OpeningHours? { return null } override fun getCurbsidePickup(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getDelivery(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getDineIn(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getReservable(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBeer(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBreakfast(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBrunch(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesDinner(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesLunch(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesVegetarianFood(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesWine(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getTakeout(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getWheelchairAccessibleEntrance(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getId(): String { return placeId } override fun getPhotoMetadatas(): MutableList { return placePhotos } override fun getSecondaryOpeningHours(): MutableList? { return null } override fun getWebsiteUri(): Uri? { return null } override fun getPhoneNumber(): String? { return null } override fun getRating(): Double? { return null } override fun getIconBackgroundColor(): Int? { return null } override fun getPriceLevel(): Int? { return null } override fun getAddressComponents(): AddressComponents? { return null } override fun getCurrentOpeningHours(): OpeningHours? { return null } override fun getAttributions(): MutableList { return mutableListOf() } override fun getAddress(): String { return placeAddress } override fun getEditorialSummary(): String? { return null } override fun getEditorialSummaryLanguageCode(): String? { return null } override fun getIconUrl(): String? { return null } override fun getPlusCode(): PlusCode? { return null } override fun getUtcOffsetMinutes(): Int? { return null } override fun getTypes(): MutableList { return placeTypes } override fun getViewport(): LatLngBounds? { return null } override fun describeContents(): Int { return 0 } override fun getLatLng(): LatLng { return placeLatLng } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/repository/googlemaps/GoogleMapsAPI.kt ================================================ package com.rtchagas.pingplacepicker.repository.googlemaps import com.rtchagas.pingplacepicker.model.SearchResult import io.reactivex.Single import retrofit2.http.GET import retrofit2.http.Query import java.util.* internal interface GoogleMapsAPI { @GET("place/nearbysearch/json?rankby=distance") fun searchNearby( @Query("location") location: String, @Query("key") apiKey: String, @Query("language") language: String = Locale.getDefault().language ): Single @GET("geocode/json") fun findByLocation( @Query("latlng") location: String, @Query("key") apiKey: String, @Query("language") language: String = Locale.getDefault().language ): Single } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/repository/googlemaps/GoogleMapsRepository.kt ================================================ package com.rtchagas.pingplacepicker.repository.googlemaps import android.annotation.SuppressLint import android.graphics.Bitmap import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.PhotoMetadata import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.api.model.PlaceLikelihood import com.google.android.libraries.places.api.net.FetchPhotoRequest import com.google.android.libraries.places.api.net.FindCurrentPlaceRequest import com.google.android.libraries.places.api.net.PlacesClient import com.rtchagas.pingplacepicker.Config import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.model.SearchResult import com.rtchagas.pingplacepicker.model.SimplePlace import com.rtchagas.pingplacepicker.repository.PlaceRepository import io.reactivex.Single import java.util.* internal class GoogleMapsRepository constructor( private val googleClient: PlacesClient, private val googleMapsAPI: GoogleMapsAPI ) : PlaceRepository { /** * Finds all nearby places ranked by likelihood of being the place where the device is. * * This call will be charged according to * [Places SDK for Android Usage and Billing](https://developers.google.com/places/android-sdk/usage-and-billing#find-current-place) */ @SuppressLint("MissingPermission") override fun getNearbyPlaces(): Single> { // Create request val request = FindCurrentPlaceRequest.builder(getPlaceFields()).build() return Single.create { emitter -> googleClient.findCurrentPlace(request).addOnCompleteListener { task -> if (task.isSuccessful) { task.result?.let { val placeList = sortByLikelihood(it.placeLikelihoods) emitter.onSuccess(placeList.map { likelihood -> likelihood.place }) } // Empty result emitter.onSuccess(listOf()) } else { emitter.tryOnError(task.exception ?: Exception("No places for you...")) } } } } /** Finds all nearby places ranked by distance from the requested location. * * This call will be charged according to * [Places SDK WEB API Usage and Billing](https://developers.google.com/maps/billing/understanding-cost-of-use#nearby-search) */ override fun getNearbyPlaces(location: LatLng): Single> { val locationParam = "${location.latitude},${location.longitude}" return googleMapsAPI.searchNearby(locationParam, PingPlacePicker.mapsApiKey) .map { searchResult -> val placeList = mutableListOf() for (simplePlace in searchResult.results) { placeList.add(mapToCustomPlace(simplePlace)) } placeList } } /** * Fetches a photo for the place. * * This call will be charged according to * [Places SDK for Android Usage and Billing](https://developers.google.com/places/android-sdk/usage-and-billing#places-photo) */ override fun getPlacePhoto(photoMetadata: PhotoMetadata): Single { // Create a FetchPhotoRequest. val photoRequest = FetchPhotoRequest.builder(photoMetadata) .setMaxWidth(Config.PLACE_IMG_WIDTH) .setMaxHeight(Config.PLACE_IMG_HEIGHT) .build() return Single.create { emitter -> googleClient.fetchPhoto(photoRequest).addOnSuccessListener { val bitmap = it.bitmap emitter.onSuccess(bitmap) }.addOnFailureListener { emitter.tryOnError(it) } } } /** * Uses Google Maps GeoLocation API to retrieve a place by its latitude and longitude. * This call will be charged according to * [Places SDK for Android Usage and Billing](https://developers.google.com/maps/documentation/geocoding/usage-and-billing#pricing-for-the-geocoding-api) */ override fun getPlaceByLocation(location: LatLng): Single { val paramLocation = "${location.latitude},${location.longitude}" return googleMapsAPI.findByLocation(paramLocation, PingPlacePicker.mapsApiKey) .map { result: SearchResult -> if (("OK" == result.status) && result.results.isNotEmpty()) { return@map mapToCustomPlace(result.results[0]) } return@map PlaceFromCoordinates(location.latitude, location.longitude) } } /** * These fields are not charged by Google. * https://developers.google.com/places/android-sdk/usage-and-billing#basic-data */ private fun getPlaceFields(): List { return listOf( Place.Field.ID, Place.Field.NAME, Place.Field.ADDRESS, Place.Field.LAT_LNG, Place.Field.TYPES, Place.Field.PHOTO_METADATAS ) } private fun mapToCustomPlace(place: SimplePlace): CustomPlace { val photoList = mutableListOf() place.photos.forEach { val photoMetadata = PhotoMetadata.builder(it.photoReference) .setAttributions(it.htmlAttributions.toString()) .setHeight(it.height) .setWidth(it.width) .build() photoList.add(photoMetadata) } val typeList = mutableListOf() place.types.forEach { simpleType -> val placeType = Place.Type.values() .find { it.name == simpleType.uppercase(Locale.US) } ?: Place.Type.OTHER typeList.add(placeType) } val latLng = LatLng(place.geometry.location.lat, place.geometry.location.lng) val address: String = place.formattedAddress.ifEmpty { place.vicinity } val name: String = buildPlaceName(place.name, address) return CustomPlace(place.placeId, name, photoList, address, typeList, latLng) } private fun buildPlaceName(originalName: String, address: String): String { // We have a nice name, use it if (originalName.isNotEmpty()) { return originalName } // Return the first part of the address, usually the street + number return address.split(",").first() } /** * Sorts the list by Likelihood. The best ranked places come first. */ private fun sortByLikelihood(placeLikelihoods: List): List { val mutableList = placeLikelihoods.toMutableList() mutableList.sortByDescending { it.likelihood } return mutableList } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/repository/googlemaps/PlaceFromCoordinates.kt ================================================ package com.rtchagas.pingplacepicker.repository.googlemaps import android.location.Location import android.net.Uri import android.os.Parcel import android.os.Parcelable import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.libraries.places.api.model.AddressComponents import com.google.android.libraries.places.api.model.OpeningHours import com.google.android.libraries.places.api.model.PhotoMetadata import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.api.model.Place.BooleanPlaceAttributeValue.UNKNOWN import com.google.android.libraries.places.api.model.PlusCode import kotlin.math.absoluteValue /** * Place without any additional info. Just latitude and longitude. */ internal class PlaceFromCoordinates( private val latitude: Double, private val longitude: Double ) : Place() { constructor(parcel: Parcel) : this( parcel.readDouble(), parcel.readDouble() ) override fun getUserRatingsTotal(): Int? { return null } /** * Default value only. * Clients shouldn't rely on this. */ override fun getBusinessStatus(): BusinessStatus { return BusinessStatus.OPERATIONAL } override fun getName(): String { return "${formatLatitude(latitude)}, ${formatLongitude(longitude)}" } override fun getOpeningHours(): OpeningHours? { return null } override fun getCurbsidePickup(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getDelivery(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getDineIn(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getReservable(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBeer(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBreakfast(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesBrunch(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesDinner(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesLunch(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesVegetarianFood(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getServesWine(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getTakeout(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getWheelchairAccessibleEntrance(): BooleanPlaceAttributeValue { return UNKNOWN } override fun getId(): String? { return null } override fun getPhotoMetadatas(): MutableList { return mutableListOf() } override fun getSecondaryOpeningHours(): MutableList? { return null } override fun getWebsiteUri(): Uri? { return null } override fun getPhoneNumber(): String? { return null } override fun getRating(): Double? { return null } override fun getIconBackgroundColor(): Int? { return null } override fun getPriceLevel(): Int? { return null } override fun getAddressComponents(): AddressComponents? { return null } override fun getCurrentOpeningHours(): OpeningHours? { return null } override fun getAttributions(): MutableList { return mutableListOf() } override fun getAddress(): String? { return null } override fun getEditorialSummary(): String? { return null } override fun getEditorialSummaryLanguageCode(): String? { return null } override fun getIconUrl(): String? { return null } override fun getPlusCode(): PlusCode? { return null } override fun getUtcOffsetMinutes(): Int? { return null } override fun getTypes(): MutableList { return mutableListOf() } override fun getViewport(): LatLngBounds? { return null } override fun getLatLng(): LatLng { return LatLng(latitude, longitude) } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeDouble(latitude) parcel.writeDouble(longitude) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): PlaceFromCoordinates { return PlaceFromCoordinates(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } // formatting methods ----------------------------------------------------------------- private fun formatLatitude(latitude: Double): String { val direction = if (latitude > 0) "N" else "S" return "${ replaceDelimiters( Location.convert( latitude.absoluteValue, Location.FORMAT_SECONDS ) ) } $direction" } private fun formatLongitude(longitude: Double): String { val direction = if (longitude > 0) "W" else "E" return "${ replaceDelimiters( Location.convert( longitude.absoluteValue, Location.FORMAT_SECONDS ) ) } $direction" } private fun replaceDelimiters(original: String): String { val parts: List = original.split(":") val degrees: String = parts[0] val minutes: String = parts[1] var seconds: String = parts[2] val idx = seconds.indexOfAny(charArrayOf(',', '.')) if (idx >= 0) { seconds = seconds.substring(0, idx) } return "${degrees}° ${minutes}' ${seconds}\"" } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/UiExtensions.kt ================================================ package com.rtchagas.pingplacepicker.ui import android.content.Context import android.view.View import android.widget.Toast import androidx.annotation.StringRes import com.jakewharton.rxbinding3.view.clicks import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import java.util.concurrent.TimeUnit internal fun View.onclick(callback: () -> Unit): Disposable = clicks() .throttleFirst(1, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { callback() } /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param message the message text. */ fun Context.toast(message: CharSequence): Toast = Toast .makeText(this, message, Toast.LENGTH_SHORT) .apply { show() } /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param resId the resource ID of the message text. */ fun Context.toast(@StringRes resId: Int): Toast = Toast .makeText(this, resId, Toast.LENGTH_SHORT) .apply { show() } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/UiUtils.kt ================================================ package com.rtchagas.pingplacepicker.ui import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration import android.util.TypedValue import androidx.annotation.ColorInt import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.R import java.util.* internal object UiUtils { /** * Gets the place drawable resource according to its type */ @SuppressLint("DiscouragedApi") fun getPlaceDrawableRes(context: Context, place: Place): Int { val defType = "drawable" val defPackage = context.packageName place.types?.let { for (type: Place.Type in it) { val name = type.name.lowercase(Locale.ENGLISH) val id: Int = context.resources.getIdentifier("ic_places_$name", defType, defPackage) if (id > 0) return id } } // Default resource return R.drawable.ic_map_marker_black_24dp } /** * Returns whether the current selected theme is night mode or not */ fun isNightModeEnabled(context: Context): Boolean { val nightMode: Int = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) return nightMode == Configuration.UI_MODE_NIGHT_YES } @ColorInt fun getColorAttr(context: Context, colorAttr: Int): Int { val typedValue = TypedValue() context.theme.resolveAttribute(colorAttr, typedValue, true) return typedValue.data } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/activity/BaseActivity.kt ================================================ package com.rtchagas.pingplacepicker.ui.activity import android.os.Bundle import android.view.LayoutInflater import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding typealias ActivityInflater = (LayoutInflater) -> T abstract class BaseActivity( private val inflate: ActivityInflater ) : AppCompatActivity() { private var _binding: T? = null /** * The view binding for this fragment. * * [https://developer.android.com/topic/libraries/view-binding] * * This property is only valid between [onCreate] and [onDestroy]. */ val binding: T get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = inflate(layoutInflater) setContentView(binding.root) } override fun onDestroy() { super.onDestroy() _binding = null } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/activity/PlacePickerActivity.kt ================================================ package com.rtchagas.pingplacepicker.ui.activity import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.location.Location import android.os.Bundle import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.doOnLayout import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.OnMapReadyCallback import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.* import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.api.model.RectangularBounds import com.google.android.libraries.places.widget.Autocomplete import com.google.android.libraries.places.widget.model.AutocompleteActivityMode import com.google.android.material.appbar.AppBarLayout import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.snackbar.Snackbar import com.google.maps.android.SphericalUtil import com.karumi.dexter.listener.PermissionDeniedResponse import com.karumi.dexter.listener.PermissionGrantedResponse import com.karumi.dexter.listener.single.BasePermissionListener import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.R import com.rtchagas.pingplacepicker.databinding.ActivityPlacePickerBinding import com.rtchagas.pingplacepicker.helper.PermissionsHelper import com.rtchagas.pingplacepicker.inject.PingKoinComponent import com.rtchagas.pingplacepicker.ui.UiUtils import com.rtchagas.pingplacepicker.ui.adapter.PlacePickerAdapter import com.rtchagas.pingplacepicker.ui.fragment.PlaceConfirmDialogFragment import com.rtchagas.pingplacepicker.ui.onclick import com.rtchagas.pingplacepicker.ui.toast import com.rtchagas.pingplacepicker.viewmodel.PlacePickerViewModel import com.rtchagas.pingplacepicker.viewmodel.Resource import io.reactivex.disposables.CompositeDisposable import org.koin.androidx.viewmodel.ext.android.viewModel import kotlin.math.abs internal class PlacePickerActivity : BaseActivity(ActivityPlacePickerBinding::inflate), PingKoinComponent, OnMapReadyCallback, GoogleMap.OnMarkerClickListener, PlaceConfirmDialogFragment.OnPlaceConfirmedListener { companion object { private const val TAG = "Ping#PlacePicker" // For passing extra parameters to this activity. const val EXTRA_LOCATION = "extra_location" const val EXTRA_RETURN_ACTUAL_LATLNG = "extra_return_actual_latlng" // Keys for storing activity state. private const val STATE_CAMERA_POSITION = "state_camera_position" private const val STATE_LOCATION = "state_location" private const val AUTOCOMPLETE_REQUEST_CODE = 1001 private const val DIALOG_CONFIRM_PLACE_TAG = "dialog_place_confirm" } private var googleMap: GoogleMap? = null private var isLocationPermissionGranted = false private var cameraPosition: CameraPosition? = null private val defaultLocation = LatLng(37.4219999, -122.0862462) private var defaultZoom = -1f private var lastKnownLocation: LatLng? = null private var placeAdapter: PlacePickerAdapter? = null private val viewModel: PlacePickerViewModel by viewModel() private val disposables = CompositeDisposable() private lateinit var fusedLocationProviderClient: FusedLocationProviderClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Configure the toolbar setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) // Check whether a pre-defined location was set. intent.getParcelableExtra(EXTRA_LOCATION)?.let { lastKnownLocation = it } // Retrieve location and camera position from saved instance state. lastKnownLocation = savedInstanceState ?.getParcelable(STATE_LOCATION) ?: lastKnownLocation cameraPosition = savedInstanceState ?.getParcelable(STATE_CAMERA_POSITION) ?: cameraPosition // Construct a FusedLocationProviderClient fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) // Sets the default zoom defaultZoom = resources.getInteger(R.integer.default_zoom).toFloat() // Initialize the UI initializeUi() // Restore any active fragment restoreFragments() // Initializes the map val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment mapFragment.getMapAsync(this) } @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if ((requestCode == AUTOCOMPLETE_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)) { data?.run { val place = Autocomplete.getPlaceFromIntent(this) moveCameraToSelectedPlace(place) showConfirmPlacePopup(place) } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_place_picker, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (android.R.id.home == item.itemId) { finishAfterTransition() return true } if (R.id.action_search == item.itemId) { requestPlacesSearch() return true } return super.onOptionsItemSelected(item) } /** * Saves the state of the map when the activity is paused. */ override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelable(STATE_CAMERA_POSITION, googleMap?.cameraPosition) outState.putParcelable(STATE_LOCATION, lastKnownLocation) } override fun onDestroy() { super.onDestroy() disposables.clear() } @SuppressLint("PotentialBehaviorOverride") override fun onMapReady(map: GoogleMap) { googleMap = map setMapStyle() map.setOnMarkerClickListener(this) checkForPermission() } override fun onMarkerClick(marker: Marker): Boolean { val place = marker.tag as Place showConfirmPlacePopup(place) return !resources.getBoolean(R.bool.auto_center_on_marker_click) } override fun onPlaceConfirmed(place: Place) { val selectedLatLng = googleMap?.cameraPosition?.target ?: LatLng(0.0, 0.0) PingPlacePicker.onPlaceSelectedListener?.onPlaceSelected(place, selectedLatLng) finishAfterTransition() } private fun adjustElevationOverlayColors() { // Set the correct elevation overlay to the CollapsingToolbarLayout val elevationOverlayProvider = ElevationOverlayProvider(this) val scrimColor: Int = elevationOverlayProvider.compositeOverlayIfNeeded( UiUtils.getColorAttr(this, R.attr.colorPrimarySurface), resources.getDimension(R.dimen.material_elevation_app_bar) ) // Set the correct elevation to the content container val containerColor = elevationOverlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded( resources.getDimension(R.dimen.material_elevation_app_bar) ) with(binding) { collapsingToolbarLayout.setContentScrimColor(scrimColor) mapContainer.setBackgroundColor(containerColor) } } private fun bindPlaces(places: List) { // Bind to the recycler view if (placeAdapter == null) { placeAdapter = PlacePickerAdapter(places) { showConfirmPlacePopup(it) } } else { placeAdapter?.swapData(places) } binding.rvNearbyPlaces.adapter = placeAdapter // Bind to the map googleMap?.run { clear() for (place in places) { place.latLng?.let { val marker: Marker? = addMarker( MarkerOptions() .position(it) .icon(getPlaceMarkerBitmap(place)) ) marker?.tag = place } } } } private fun checkForPermission() { PermissionsHelper.checkForLocationPermission(this, object : BasePermissionListener() { override fun onPermissionDenied(response: PermissionDeniedResponse?) { isLocationPermissionGranted = false initMap() } override fun onPermissionGranted(response: PermissionGrantedResponse?) { isLocationPermissionGranted = true initMap() } }) } private fun getCurrentLatLngBounds(): LatLngBounds { val radius = resources.getInteger(R.integer.autocomplete_search_bias_radius).toDouble() val location: LatLng = lastKnownLocation ?: defaultLocation val northEast: LatLng = SphericalUtil.computeOffset(location, radius, 45.0) val southWest: LatLng = SphericalUtil.computeOffset(location, radius, 225.0) return LatLngBounds(southWest, northEast) } private fun getDeviceLocation(animate: Boolean) = try { // Get the best and most recent location of the device, which may be null in rare // cases when a location is not available. fusedLocationProviderClient.lastLocation .addOnFailureListener(this) { setDefaultLocation() } .addOnSuccessListener(this) { location: Location? -> // In rare cases location may be null... if (location == null) { retryWhenLocationIsNotAvailable(animate) return@addOnSuccessListener } // Set the map's camera position to the current location of the device. val latLng = LatLng(location.latitude, location.longitude) lastKnownLocation = latLng val update = CameraUpdateFactory.newLatLngZoom(latLng, defaultZoom) if (animate) { googleMap?.animateCamera(update) } else { googleMap?.moveCamera(update) } // Load the places near this location loadNearbyPlaces() } } catch (e: SecurityException) { Log.e(TAG, e.toString()) } @Suppress("DEPRECATION") private fun getPlaceMarkerBitmap(place: Place): BitmapDescriptor { val innerIconSize: Int = resources.getDimensionPixelSize(R.dimen.marker_inner_icon_size) val bgDrawable = ResourcesCompat.getDrawable( resources, R.drawable.ic_map_marker_solid_red_32dp, null )!! val fgDrawable = ResourcesCompat.getDrawable( resources, UiUtils.getPlaceDrawableRes(this, place), null )!! DrawableCompat.setTint(fgDrawable, resources.getColor(R.color.colorMarkerInnerIcon)) val bitmap = Bitmap.createBitmap( bgDrawable.intrinsicWidth, bgDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) bgDrawable.setBounds(0, 0, canvas.width, canvas.height) val left = (canvas.width - innerIconSize) / 2 val top = (canvas.height - innerIconSize) / 3 val right = left + innerIconSize val bottom = top + innerIconSize fgDrawable.setBounds(left, top, right, bottom) bgDrawable.draw(canvas) fgDrawable.draw(canvas) return BitmapDescriptorFactory.fromBitmap(bitmap) } private fun handlePlaceByLocation(result: Resource) { binding.pbLoading.hide() when (result.status) { Resource.Status.LOADING -> binding.pbLoading.show() Resource.Status.SUCCESS -> result.data?.run { showConfirmPlacePopup(this) } Resource.Status.ERROR -> toast(R.string.picker_load_this_place_error) Resource.Status.NO_DATA -> Log.d(TAG, "No places data found...") } } private fun handlePlacesLoaded(result: Resource>) { binding.pbLoading.hide() when (result.status) { Resource.Status.LOADING -> binding.pbLoading.show() Resource.Status.SUCCESS -> bindPlaces((result.data ?: listOf())) Resource.Status.ERROR -> toast(R.string.picker_load_places_error) Resource.Status.NO_DATA -> Log.d(TAG, "No places data found...") } } private fun initializeUi() = with(binding) { // Some material components still don't support setting the correct // elevation for dark themes, so we should handle that adjustElevationOverlayColors() // Initialize the recycler view rvNearbyPlaces.layoutManager = LinearLayoutManager(this@PlacePickerActivity) // Bind the click listeners disposables.addAll( btnMyLocation.onclick { getDeviceLocation(true) }, btnRefreshLocation.onclick { refreshNearbyPlaces() }, cardSearch.onclick { requestPlacesSearch() }, mapContainer.onclick { selectThisPlace() } ) // Hide or show the refresh places button according to nearby search flag btnRefreshLocation.isVisible = PingPlacePicker.isNearbySearchEnabled // Hide or show the card search according to the width cardSearch.isVisible = resources.getBoolean(R.bool.show_card_search) // Add a nice fade effect to toolbar appBarLayout.addOnOffsetChangedListener { appBarLayout, verticalOffset -> toolbar.alpha = abs(verticalOffset / appBarLayout.totalScrollRange.toFloat()) } // Disable vertical scrolling on appBarLayout (it messes with the map...) // Set default behavior val appBarLayoutParams = appBarLayout.layoutParams as CoordinatorLayout.LayoutParams appBarLayoutParams.behavior = AppBarLayout.Behavior() // Disable the drag val behavior = appBarLayoutParams.behavior as AppBarLayout.Behavior behavior.setDragCallback(object : AppBarLayout.Behavior.DragCallback() { override fun canDrag(appBarLayout: AppBarLayout): Boolean { return false } }) // Set the size of AppBarLayout to 68% of the total height coordinator.doOnLayout { val size: Int = (it.height * 68) / 100 appBarLayoutParams.height = size } } private fun initMap() { // Turn on/off the My Location layer and the related control on the map updateLocationUI() // Restore any saved state restoreMapState() if (isLocationPermissionGranted) { if (lastKnownLocation == null) { // Get the current location of the device and set the position of the map getDeviceLocation(false) } else { // Use the last know location to point the map to setDefaultLocation() loadNearbyPlaces() } } else { setDefaultLocation() } } private fun loadNearbyPlaces() { viewModel.getNearbyPlaces(lastKnownLocation ?: defaultLocation) .observe(this) { handlePlacesLoaded(it) } } private fun moveCameraToSelectedPlace(place: Place) { place.latLng?.let { googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(it, defaultZoom)) } } private fun refreshNearbyPlaces() { googleMap?.cameraPosition?.run { viewModel.getNearbyPlaces(target) .observe(this@PlacePickerActivity) { handlePlacesLoaded(it) } } } private fun requestPlacesSearch() { // This only works if location permission is granted if (!isLocationPermissionGranted) { checkForPermission() return } // Places API needs a location as well... if (lastKnownLocation == null) { return } // These fields are not charged by Google: // https://developers.google.com/places/android-sdk/usage-and-billing#basic-data val placeFields = listOf( Place.Field.ID, Place.Field.NAME, Place.Field.ADDRESS, Place.Field.LAT_LNG, Place.Field.TYPES, Place.Field.PHOTO_METADATAS ) val rectangularBounds = RectangularBounds.newInstance(getCurrentLatLngBounds()) // Start the autocomplete intent. val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, placeFields) .setLocationBias(rectangularBounds) .build(this) startActivityForResult(intent, AUTOCOMPLETE_REQUEST_CODE) } private fun restoreFragments() { val confirmFragment = supportFragmentManager .findFragmentByTag(DIALOG_CONFIRM_PLACE_TAG) as PlaceConfirmDialogFragment? confirmFragment?.run { confirmListener = this@PlacePickerActivity } } private fun restoreMapState() { cameraPosition?.run { googleMap?.moveCamera(CameraUpdateFactory.newCameraPosition(this)) } } private fun retryWhenLocationIsNotAvailable(animate: Boolean) { // Location is not available. Ask the user to retry. setDefaultLocation() Snackbar .make(binding.root, R.string.picker_location_unavailable, Snackbar.LENGTH_INDEFINITE) .setAction(R.string.places_try_again) { getDeviceLocation(animate) } .show() } private fun selectThisPlace() { googleMap?.cameraPosition?.run { viewModel.getPlaceByLocation(target) .observe(this@PlacePickerActivity) { handlePlaceByLocation(it) } } } private fun setDefaultLocation() { val default: LatLng = lastKnownLocation ?: defaultLocation googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(default, defaultZoom)) } /** * Customise the styling of the base map using a JSON object defined in a raw resource file. */ private fun setMapStyle() { if (!UiUtils.isNightModeEnabled(this)) return try { googleMap?.run { val success = setMapStyle( MapStyleOptions.loadRawResourceStyle( this@PlacePickerActivity, R.raw.maps_night_style ) ) if (!success) { Log.e(TAG, "Style parsing failed.") } } } catch (e: Exception) { Log.e(TAG, "Can't style the map", e) } } private fun showConfirmPlacePopup(place: Place) { val fragment = PlaceConfirmDialogFragment.newInstance(place, this) fragment.show(supportFragmentManager, DIALOG_CONFIRM_PLACE_TAG) } @SuppressLint("MissingPermission") private fun updateLocationUI() { googleMap?.let { with(it.uiSettings) { isMyLocationButtonEnabled = false isMapToolbarEnabled = false } if (isLocationPermissionGranted) { it.isMyLocationEnabled = true binding.btnMyLocation.visibility = View.VISIBLE } else { binding.btnMyLocation.visibility = View.GONE it.isMyLocationEnabled = false } } } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/adapter/PlacePickerAdapter.kt ================================================ package com.rtchagas.pingplacepicker.ui.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.databinding.ItemPlaceBinding import com.rtchagas.pingplacepicker.ui.UiUtils internal class PlacePickerAdapter( private var placeList: List, private val clickListener: (Place) -> Unit ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = ItemPlaceBinding.inflate(inflater, parent, false) return PlaceViewHolder(binding) } override fun onBindViewHolder(holder: PlaceViewHolder, position: Int) { holder.bind(placeList[position], clickListener) } override fun getItemCount(): Int = placeList.size fun swapData(newPlaceList: List) { placeList = newPlaceList notifyDataSetChanged() } inner class PlaceViewHolder(private val binding: ItemPlaceBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(place: Place, listener: (Place) -> Unit) { with(binding) { root.setOnClickListener { listener(place) } ivPlaceType.setImageResource(UiUtils.getPlaceDrawableRes(itemView.context, place)) tvPlaceName.text = place.name tvPlaceAddress.text = place.address } } } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/ui/fragment/PlaceConfirmDialogFragment.kt ================================================ package com.rtchagas.pingplacepicker.ui.fragment import android.app.Dialog import android.content.Context import android.graphics.Bitmap import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.core.view.isVisible import androidx.transition.TransitionManager import coil.load import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.Config import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.R import com.rtchagas.pingplacepicker.databinding.FragmentDialogPlaceConfirmBinding import com.rtchagas.pingplacepicker.helper.UrlSignerHelper import com.rtchagas.pingplacepicker.inject.PingKoinComponent import com.rtchagas.pingplacepicker.ui.UiUtils import com.rtchagas.pingplacepicker.viewmodel.PlaceConfirmDialogViewModel import com.rtchagas.pingplacepicker.viewmodel.Resource import org.koin.androidx.viewmodel.ext.android.viewModel import java.util.* internal class PlaceConfirmDialogFragment : AppCompatDialogFragment(), PingKoinComponent { private var _binding: FragmentDialogPlaceConfirmBinding? = null private val binding: FragmentDialogPlaceConfirmBinding get() = _binding!! private val viewModel: PlaceConfirmDialogViewModel by viewModel() private lateinit var place: Place var confirmListener: OnPlaceConfirmedListener? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Check mandatory parameters for this fragment if (requireArguments().getParcelable(ARG_PLACE) == null) { throw IllegalArgumentException("You must pass a Place as argument to this fragment") } arguments?.run { place = getParcelable(ARG_PLACE)!! } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireActivity()) builder.setTitle(R.string.picker_place_confirm) .setView(getContentView(requireContext())) .setPositiveButton(android.R.string.ok) { _, _ -> confirmListener?.onPlaceConfirmed(place) dismiss() } .setNegativeButton(R.string.picker_place_confirm_cancel) { _, _ -> // Just dismiss here... dismiss() } return builder.create() } override fun onDestroyView() { super.onDestroyView() _binding = null } private fun getContentView(context: Context): View { _binding = FragmentDialogPlaceConfirmBinding.inflate(LayoutInflater.from(context)) initializeUi() return binding.root } private fun initializeUi() = with(binding) { if (place.name.isNullOrEmpty()) { tvPlaceName.isVisible = false } else { tvPlaceName.text = place.name } tvPlaceAddress.text = place.address fetchPlaceMap() fetchPlacePhoto() } private fun fetchPlaceMap() = with(binding.ivPlaceMap) { isVisible = if (resources.getBoolean(R.bool.show_confirmation_map)) true else return@with val staticMapUrl = getFinalMapUrl() binding.ivPlaceMap.load(staticMapUrl) { listener( onError = { request, error -> isVisible = false Log.w(TAG, "Error loading map image: ${request.data}", error.throwable) } ) } } private fun fetchPlacePhoto() { val photoMetadata = place.photoMetadatas?.firstOrNull() if (resources.getBoolean(R.bool.show_confirmation_photo) && (photoMetadata != null) ) { viewModel.getPlacePhoto(photoMetadata) .observe(this) { handlePlacePhotoLoaded(it) } } else { handlePlacePhotoLoaded(Resource.noData()) } } private fun getFinalMapUrl(): String { var mapUrl = Config.STATIC_MAP_URL .format( place.latLng?.latitude, place.latLng?.longitude, PingPlacePicker.mapsApiKey, Locale.getDefault().language ) if (UiUtils.isNightModeEnabled(requireContext())) { mapUrl += Config.STATIC_MAP_URL_STYLE_DARK } if (PingPlacePicker.urlSigningSecret.isNotEmpty()) { // Sign the URL return UrlSignerHelper.signUrl(mapUrl, PingPlacePicker.urlSigningSecret) } return mapUrl } private fun handlePlacePhotoLoaded(result: Resource) = with(binding.ivPlacePhoto) { if (result.status == Resource.Status.SUCCESS) { TransitionManager.beginDelayedTransition(binding.root) visibility = View.VISIBLE setImageBitmap(result.data) } else { visibility = View.GONE } } /** * Listener called when a place is updated. */ interface OnPlaceConfirmedListener { fun onPlaceConfirmed(place: Place) } companion object { private const val TAG = "Ping#PlaceConfirmDialog" private const val ARG_PLACE = "arg_place" fun newInstance( place: Place, listener: OnPlaceConfirmedListener ): PlaceConfirmDialogFragment { val args = Bundle() args.putParcelable(ARG_PLACE, place) return PlaceConfirmDialogFragment().apply { arguments = args confirmListener = listener } } } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/viewmodel/BaseViewModel.kt ================================================ package com.rtchagas.pingplacepicker.viewmodel import androidx.lifecycle.ViewModel import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable /** * ViewModel that automatically disposes * registered [org.reactivestreams.Publisher]s */ internal abstract class BaseViewModel : ViewModel() { private val compositeDisposable = CompositeDisposable() override fun onCleared() { super.onCleared() clearDisposables() } fun addDisposable(disposable: Disposable) { compositeDisposable.add(disposable) } private fun clearDisposables() { compositeDisposable.clear() } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/viewmodel/PlaceConfirmDialogViewModel.kt ================================================ package com.rtchagas.pingplacepicker.viewmodel import android.graphics.Bitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.android.libraries.places.api.model.PhotoMetadata import com.rtchagas.pingplacepicker.repository.PlaceRepository import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers internal class PlaceConfirmDialogViewModel( private val repository: PlaceRepository ) : BaseViewModel() { private val placePhotoLiveData: MutableLiveData> = MutableLiveData() fun getPlacePhoto(photoMetadata: PhotoMetadata): LiveData> { // If we already loaded the places for this location, return the same live data // instead of fetching (and charging) again. placePhotoLiveData.value?.run { return placePhotoLiveData } val disposable: Disposable = repository.getPlacePhoto(photoMetadata) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { placePhotoLiveData.value = Resource.loading() } .subscribe( { result: Bitmap -> placePhotoLiveData.value = Resource.success(result) }, { error: Throwable -> placePhotoLiveData.value = Resource.error(error) } ) // Keep track of this disposable during the ViewModel lifecycle addDisposable(disposable) return placePhotoLiveData } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/viewmodel/PlacePickerViewModel.kt ================================================ package com.rtchagas.pingplacepicker.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.repository.PlaceRepository import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers internal class PlacePickerViewModel(private val repository: PlaceRepository) : BaseViewModel() { // Keep the place list in this view model state private val placeList: MutableLiveData>> = MutableLiveData() private var lastLocation: LatLng = LatLng(0.0, 0.0) fun getNearbyPlaces(location: LatLng): LiveData>> { // If we already loaded the places for this location, return the same live data // instead of fetching (and charging) again. placeList.value?.run { if (lastLocation == location) return placeList } // Update the last fetched location lastLocation = location val placeQuery = if (PingPlacePicker.isNearbySearchEnabled) repository.getNearbyPlaces(location) else repository.getNearbyPlaces() val disposable: Disposable = placeQuery .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { placeList.value = Resource.loading() } .subscribe( { result: List -> placeList.value = Resource.success(result) }, { error: Throwable -> placeList.value = Resource.error(error) } ) // Keep track of this disposable during the ViewModel lifecycle addDisposable(disposable) return placeList } fun getPlaceByLocation(location: LatLng): LiveData> { val liveData = MutableLiveData>() val disposable: Disposable = repository.getPlaceByLocation(location) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { liveData.value = Resource.loading() } .subscribe( { result: Place? -> liveData.value = Resource.success(result) }, { error: Throwable -> liveData.value = Resource.error(error) } ) // Keep track of this disposable during the ViewModel lifecycle addDisposable(disposable) return liveData } } ================================================ FILE: library/src/main/java/com/rtchagas/pingplacepicker/viewmodel/Resource.kt ================================================ package com.rtchagas.pingplacepicker.viewmodel /** * Resource holder provided to the UI */ internal class Resource private constructor( val status: Status, val data: T?, val error: Throwable? ) { /** * Possible status types of a response provided to the UI */ enum class Status { LOADING, SUCCESS, ERROR, NO_DATA } companion object { fun loading(): Resource { return Resource(Status.LOADING, null, null) } fun success(data: T): Resource { return Resource(Status.SUCCESS, data, null) } fun error(error: Throwable?): Resource { return Resource(Status.ERROR, null, error) } fun noData(): Resource { return Resource(Status.NO_DATA, null, null) } } } ================================================ FILE: library/src/main/res/drawable/bg_button_round.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_arrow_back_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_close_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_crosshairs_gps_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_magnify_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_magnify_toolbar_menu_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_map_marker_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_map_marker_radius_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_map_marker_select_red_48dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_map_marker_solid_red_32dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_accounting.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_airport.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_amusement_park.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_aquarium.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_art_gallery.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_atm.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bakery.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bank.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bar.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_beauty_salon.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bicycle_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_book_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bowling_alley.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_bus_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_cafe.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_campground.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_car_dealer.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_car_rental.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_car_repair.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_car_wash.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_casino.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_cemetery.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_church.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_city_hall.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_clothing_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_convenience_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_courthouse.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_dentist.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_department_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_doctor.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_electrician.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_electronics_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_embassy.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_establishment.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_finance.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_fire_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_florist.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_food.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_funeral_home.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_furniture_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_gas_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_gym.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_hair_care.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_hardware_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_health.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_hindu_temple.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_home_goods_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_hospital.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_insurance_agency.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_jewelry_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_laundry.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_lawyer.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_library.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_liquor_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_local_government_office.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_locksmith.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_lodging.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_meal_takeaway.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_mosque.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_movie_rental.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_movie_theater.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_moving_company.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_museum.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_night_club.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_painter.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_park.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_parking.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_pet_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_pharmacy.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_physiotherapist.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_place_of_worship.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_plumber.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_police.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_post_office.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_real_estate_agency.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_restaurant.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_roofing_contractor.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_rv_park.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_school.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_shoe_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_shopping_mall.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_spa.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_stadium.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_storage.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_store.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_subway_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_supermarket.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_synagogue.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_taxi_stand.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_train_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_transit_station.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_travel_agency.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_veterinary_care.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_places_zoo.xml ================================================ ================================================ FILE: library/src/main/res/drawable/ic_refresh_black_24dp.xml ================================================ ================================================ FILE: library/src/main/res/drawable/wrapper_places_powered_by_google.xml ================================================ ================================================ FILE: library/src/main/res/drawable-night/wrapper_places_powered_by_google.xml ================================================ ================================================ FILE: library/src/main/res/layout/activity_place_picker.xml ================================================ ================================================ FILE: library/src/main/res/layout/fragment_dialog_place_confirm.xml ================================================ ================================================ FILE: library/src/main/res/layout/item_place.xml ================================================ ================================================ FILE: library/src/main/res/layout/places_autocomplete_impl_fragment_overlay.xml ================================================ ================================================ FILE: library/src/main/res/menu/menu_place_picker.xml ================================================ ================================================ FILE: library/src/main/res/raw/maps_night_style.json ================================================ [ { "elementType": "geometry", "stylers": [ { "color": "#242f3e" } ] }, { "elementType": "labels.text.fill", "stylers": [ { "color": "#746855" } ] }, { "elementType": "labels.text.stroke", "stylers": [ { "color": "#242f3e" } ] }, { "featureType": "administrative.locality", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "poi", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "poi.park", "elementType": "geometry", "stylers": [ { "color": "#263c3f" } ] }, { "featureType": "poi.park", "elementType": "labels.text.fill", "stylers": [ { "color": "#6b9a76" } ] }, { "featureType": "road", "elementType": "geometry", "stylers": [ { "color": "#38414e" } ] }, { "featureType": "road", "elementType": "geometry.stroke", "stylers": [ { "color": "#212a37" } ] }, { "featureType": "road", "elementType": "labels.text.fill", "stylers": [ { "color": "#9ca5b3" } ] }, { "featureType": "road.highway", "elementType": "geometry", "stylers": [ { "color": "#746855" } ] }, { "featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [ { "color": "#1f2835" } ] }, { "featureType": "road.highway", "elementType": "labels.text.fill", "stylers": [ { "color": "#f3d19c" } ] }, { "featureType": "transit", "elementType": "geometry", "stylers": [ { "color": "#2f3948" } ] }, { "featureType": "transit.station", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "water", "elementType": "geometry", "stylers": [ { "color": "#17263c" } ] }, { "featureType": "water", "elementType": "labels.text.fill", "stylers": [ { "color": "#515c6d" } ] }, { "featureType": "water", "elementType": "labels.text.stroke", "stylers": [ { "color": "#17263c" } ] } ] ================================================ FILE: library/src/main/res/values/colors.xml ================================================ @color/material_bluegrey500 @color/material_bluegrey800 @color/material_white @color/material_pink500 @color/material_pink800 @color/material_white @color/material_grey200 @color/material_black @color/material_white @color/material_black @color/material_on_surface_emphasis_high_type @color/material_on_surface_emphasis_medium @color/material_red400 @color/material_white @color/textColorPrimary @color/colorSecondary @color/textColorSecondary @color/colorSecondary @color/colorPrimary ================================================ FILE: library/src/main/res/values/config.xml ================================================ false true true 17 5000 false true ================================================ FILE: library/src/main/res/values/dimens.xml ================================================ 24dp 32dp 48dp 140dp 16dp 16dp 70dp 16dp 20dp 48dp @dimen/default_margin 8dp 16dp 60dp 24dp 10dp ================================================ FILE: library/src/main/res/values/strings.xml ================================================ Ping Place Picker Select a location Select this location Or choose a nearby place "Couldn't load nearby places…" "Couldn't find the selected place…" Use this place? Change location Location is unavailable A photo of the selected place A map of the selected place Location Permission We only need your location to help you find relevant nearby places. ================================================ FILE: library/src/main/res/values/styles.xml ================================================ ================================================ FILE: library/src/main/res/values-w600dp/config.xml ================================================ false ================================================ FILE: library/src/main/res/values-zh-rTW/strings.xml ================================================ 選擇附近的地點 選取這個位置 或選擇附近的地點 "無法載入附近地點" "找不到被選擇的地點" 要使用這個位置嗎? 變更位置 位置不可用 A photo of the selected place A map of the selected place 定位權限 我們需要你的定位,以便協助你快速找到地點 ================================================ FILE: library/src/test/java/com/rtchagas/pingplacepicker/ExampleUnitTest.java ================================================ package com.rtchagas.pingplacepicker; import org.junit.Test; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ExampleUnitTest { @Test public void addition_isCorrect() { assertEquals(4, 2 + 2); } } ================================================ FILE: sample/.gitignore ================================================ /build ================================================ FILE: sample/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { namespace 'com.rtchagas.pingsample' compileSdk 33 defaultConfig { applicationId "com.rtchagas.pingsample" minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } kotlin { jvmToolchain(11) } buildFeatures { viewBinding true } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':library') // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Support library implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.material:material:1.9.0' // Places library implementation 'com.google.android.libraries.places:places:3.2.0' // Other implementation 'com.github.mcginty:material-colors:1.1.0' testImplementation 'junit:junit:4.13.2' } ================================================ FILE: sample/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/java/com/rtchagas/pingsample/MainActivity.kt ================================================ package com.rtchagas.pingsample import android.os.Bundle import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.Place import com.rtchagas.pingplacepicker.PingPlacePicker import com.rtchagas.pingplacepicker.ui.activity.BaseActivity import com.rtchagas.pingplacepicker.ui.toast import com.rtchagas.pingsample.databinding.ActivityMainBinding class MainActivity : BaseActivity(ActivityMainBinding::inflate), PingPlacePicker.OnPlaceSelectedListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding.btnOpenPlacePicker.setOnClickListener { showPlacePicker() } } private fun showPlacePicker() { val builder = PingPlacePicker.Builder() builder.setAndroidApiKey(getString(R.string.key_google_apis_android)) .setMapsApiKey(getString(R.string.key_google_apis_maps)) .setOnPlaceSelectedListener(this) // If you want to set a initial location // rather then the current device location. // pingBuilder.setLatLng(LatLng(37.4219999, -122.0862462)) try { val pingIntent = builder.build(this) startActivity(pingIntent) } catch (ex: Exception) { toast("Google Play Services is not Available") } } override fun onPlaceSelected(place: Place, latLng: LatLng) { toast("You selected: ${place.name}\n Map location: $latLng") } } ================================================ FILE: sample/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: sample/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: sample/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: sample/src/main/res/values/colors.xml ================================================ @color/material_teal500 @color/material_teal800 @color/material_white @color/material_deeporange500 @color/material_deeporange800 @color/material_white @color/material_grey200 @color/material_black @color/material_white @color/material_black @color/material_on_surface_emphasis_high_type @color/material_on_surface_emphasis_medium @color/material_deeporange400 @color/material_white ================================================ FILE: sample/src/main/res/values/keys.xml ================================================ "your android key" "your maps key" ================================================ FILE: sample/src/main/res/values/strings.xml ================================================ Ping Sample Ping! ================================================ FILE: sample/src/main/res/values/styles.xml ================================================ ================================================ FILE: sample/src/main/res/values-night/colors.xml ================================================ @color/material_teal300 @color/colorSurface @color/material_black @color/material_deeporange200 @color/material_deeporange300 @color/material_black @color/colorSurface @color/colorOnSurface #202125 @color/material_white @color/material_on_surface_emphasis_high_type @color/material_on_surface_emphasis_medium @color/material_deeporange200 @color/colorSurface ================================================ FILE: sample/src/test/java/com/rtchagas/pingsample/ExampleUnitTest.kt ================================================ package com.rtchagas.pingsample import org.junit.Test import org.junit.Assert.* /** * Example local unit test, which will execute on the development machine (host). * * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } ================================================ FILE: settings.gradle ================================================ plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' } include ':library', ':sample'