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/#rtchagas/pingplacepicker) [](#)
If you're here looking for a place picker you have probably read this:

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.
## 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.

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-ar/strings.xml
================================================
اختر موقع
اختيار هذا الموقع
او اختيار مكان مجاور
"لا يمكن تحميل الاماكن المجاورة…"
"لا يمكن العثور على المكان المحدد…"
استخدام هذا المكان؟
تغيير الموقع
الموقع غير متاح
صورة المكان المحدد
خريطة المكان المحدد
اذن الموقع
نحن فى حاجة الى موقعك حتى نستطيع العثور على الاماكن المجاورة المرتبطة بك.
================================================
FILE: library/src/main/res/values-ca/strings.xml
================================================
Tria una ubicació
Escull aquesta ubicació
O escull un lloc proper
No es pot carregar els llocs propers ...
No es pot trobar la ubicació seleccionada ...
Fer servir aquesta ubicació?
Canvia la ubicació
Ubicació no disponible
Una fotografia de la ubicació seleccionada
El mapa de la ubicació seleccionada
Permís de localització
Necessitem la teva localització per ajudar-te a trobar llocs propers
================================================
FILE: library/src/main/res/values-cs/strings.xml
================================================
Vyberte místo
Vybrat toto umístění
Nebo vyberte místo v okolí
Nemohu načíst místa v okolí…
"Nemohu najít vybrané místo…"
Zvolit toto místo?
Změnit umístění
Umístění není dostupné
Obrázek zvoleného místa
Mapa zvoleného místa
Oprávnění k přístupu k poloze
Potřebujeme znát vaši polohu, abychom mohli najít vhodná místa v okolí.
================================================
FILE: library/src/main/res/values-de/strings.xml
================================================
Einen Ort auswählen
Diesen Ort auswählen
Ort ändern
Diesen Ort verwenden?
Oder Ort in der Nähe auswählen
Ort ist nicht verfügbar
Konnte den ausgewählten Ort nicht finden…
Konnte Orte in der Nähe nicht laden…
Standort-Berechtigungen
Wir benötigen Ihre Orte nur um Ihnen dabei zu helfen Orte in der Nähe zu finden.
Eine Karte des ausgewählten Ortes
Ein Foto des ausgewählten Ortes
================================================
FILE: library/src/main/res/values-es-rES/strings.xml
================================================
Escoge una ubicación
Escoge esta ubicación
O escoje un lugar cercano
No se puede cargar los lugares cercanos...
No se puede encontrar la ubicación seleccionada...
¿Usar esta ubicación?
Cambia la ubicación
Ubicación no disponible
Una fotografía de la ubicación seleccionada
El mapa de la ubicación seleccionada
Permiso de localización
Necesitamos tu localización para ayudarte a encontrar sitios cercanos
================================================
FILE: library/src/main/res/values-ko-rKR/strings.xml
================================================
위치 선택
현재 위치 선택
근처 장소
"근처 장소를 확인할 수 없습니다."
"선택한 장소를 찾을 수 없습니다."
이 장소를 사용 하시겠습니까?
장소 변경
위치를 사용할 수 없습니다.
선택한 장소의 사진
선택한 장소의 지도
위치 권한
주변 장소를 찾기 위해 위치 권한이 필요합니다.
================================================
FILE: library/src/main/res/values-pt/strings.xml
================================================
"Imagem do local "
Mapa do local selecionado
Precisamos da sua localização apenas para encontrar locais relevantes ao seu redor.
Permissão de Localização
Não foi possível encontrar locais ao redor.
" Não foi possível encontrar o local selecionado…"
Localização não está disponível
Ou escolha um local ao redor
Utilizar este local?
Alterar localização
Selecionar este local
Escolha um local
================================================
FILE: library/src/main/res/values-tr/strings.xml
================================================
Select a location
Bu konumu seç
İsterseniz yakındaki bir yeri seçin
"Yakında bir yer bulunamadı…"
"Seçtiğiniz yer bulunamadı…"
Bu konum kullanılsın mı?
Konumu Değiştir
Lokasyon Bulunamadı
Seçilen yerin fotoğrafı
Seçilen yerin haritası
Konum İzini
Yakındaki yerleri gösterebilmek için konum izinine ihtiyacımız var.
================================================
FILE: library/src/main/res/values-v21/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'