diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0233a71 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ferranpons diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..cfca4fa --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at engineers@scmspain.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..d93d2ba --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,14 @@ +Contribute +---------- + +If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. + +When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. + +1. Create an issue to discuss about your idea +2. [Fork it] (https://github.com/multiplatformkickstarter/leku-multiplatform/fork) +3. Create your feature branch (`git checkout -b my-new-feature`) +4. Commit your changes (`git commit -am 'Add some feature'`) +5. Push to the branch (`git push origin my-new-feature`) +6. Create a new Pull Request +7. Profit! :white_check_mark: diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..05b4ffb --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ +## Description + +## Info Required + +- Which version of the library do you actually use? + +- Do you have the localization permission granted? + +- Are you sending parameters to the activity through the bundle? + +- Could you describe what are the actions do you make to raise that error? + +- Android monitor shows any log related to the library when the error is shown? + +- Any other thing that could help me to reproduce the error? + + +## Screenshots + diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..886c59b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** + +**Info Required** +- Which version of the library do you actually use? + +- Do you have the localization permission granted? + +- Are you sending parameters to the activity through the bundle? + +- Android monitor shows any log related to the library when the error is shown? + +- Any other thing that could help me to reproduce the error? + +**Screenshots** + +**Smartphone:** + - Device: [e.g. Samsung S8] + +**Additional context** diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..066b2d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..9df504d --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,17 @@ +addReviewers: true +addAssignees: true +reviewers: +- marcserrascmspain +- gerardpedreny +- jlquintana + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +numberOfReviewers: 2 + +assignees: + - ferranpons + +# A list of keywords to be skipped the process that add reviewers if pull requests include it +skipKeywords: + - wip diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..802b6af --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,11 @@ +name-template: v$NEXT_PATCH_VERSION +categories: +- title: 🚀 Features + label: new feature +- title: 🐛 Bug Fixes + label: bug +tag-template: - $TITLE @$AUTHOR (#$NUMBER) +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..36c6da2 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,30 @@ +name: Android CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Detekt Check + run: ./gradlew detekt + - name: ktLink Check + run: ./gradlew ktlintCheck + - name: Build with Gradle + run: ./gradlew build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..89206f4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,46 @@ +name: Publish + +on: + release: + # We'll run this workflow when a new GitHub release is created + types: [released] + +jobs: + publish: + name: Release build and publish + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + # Base64 decodes and pipes the GPG key content into the secret file + - name: Prepare environment + env: + GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} + SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} + run: | + git fetch --unshallow + sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" + + # Builds the release artifacts of the library + - name: Release build + run: ./gradlew assembleRelease -x :app:assembleRelease + + # Generates other artifacts (javadocJar is optional) + - name: Source jar + run: ./gradlew androidSourcesJar javadocJar + + # Runs upload, and then closes & releases the repository + - name: Publish to MavenCentral + run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERTOKEN_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_USERTOKEN_PASSWORD }} + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1cb5e1c..a12295c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ captures/ output.json # IntelliJ -*.iml .idea/ misc.xml deploymentTargetDropDown.xml @@ -38,4 +37,40 @@ xcuserdata *.klib .kotlin/metadata -.kotlin/sessions \ No newline at end of file +.kotlin/sessions + +build/ +/*/bin/ +/*/gen/ +/*/proguard/ +/*/out/ +/*/.svn/ +/*/.settings/ +*.ap_ +*.dex +project.properties +androidApp/src/debug/ +androidApp/src/debug/*.xml + +.idea/.name +.idea/*.xml +.idea/dataSources.ids +.idea/scopes/ +.idea/inspectionProfiles/ +.idea/*/ +.metadata/ + +Thumbs.db + +atlassian-ide-plugin.xml + +/*/mapping.txt +/*/seeds.txt +/*/unused.txt + + +.gradle/* + +projectFilesBackup/.idea/*.xml + +site/ diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 6301a24..0be7e01 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -1,6 +1,9 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.compiler) } @@ -28,19 +31,33 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JvmTarget.JVM_21.target } } dependencies { - implementation(projects.shared) - implementation(libs.compose.ui) - implementation(libs.compose.ui.tooling.preview) - implementation(libs.compose.material3) + implementation(libs.androidx.multidex) + implementation(libs.play.services.maps) + + // Legacy material library used for Theme.MaterialComponent in XML + implementation(libs.google.material) + + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.animation) + implementation(libs.androidx.activity.compose) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.androidx.lifecycle.viewmodel.compose) + + implementation(projects.shared) + debugImplementation(libs.compose.ui.tooling) } \ No newline at end of file diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 22d1fac..43f242a 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -1,10 +1,35 @@ - + + + + + + + + + + + tools:ignore="GoogleAppIndexingWarning"> + + + + + @@ -13,5 +38,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/MainActivity.kt b/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/MainActivity.kt index 2bc1e65..39c3023 100644 --- a/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/MainActivity.kt +++ b/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/MainActivity.kt @@ -1,40 +1,342 @@ package com.multiplatformkickstarter.leku.android +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.location.Address +import android.location.Location import android.os.Bundle +import android.os.StrictMode +import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.* +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.multiplatformkickstarter.leku.Greeting +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.multiplatformkickstarter.leku.ADDRESS +import com.multiplatformkickstarter.leku.LATITUDE +import com.multiplatformkickstarter.leku.LEKU_POI +import com.multiplatformkickstarter.leku.LOCATION_ADDRESS +import com.multiplatformkickstarter.leku.LONGITUDE +import com.multiplatformkickstarter.leku.LekuPoi +import com.multiplatformkickstarter.leku.LocationPicker +import com.multiplatformkickstarter.leku.LocationPickerActivity +import com.multiplatformkickstarter.leku.TIME_ZONE_DISPLAY_NAME +import com.multiplatformkickstarter.leku.TIME_ZONE_ID +import com.multiplatformkickstarter.leku.TRANSITION_BUNDLE +import com.multiplatformkickstarter.leku.ZIPCODE +import com.multiplatformkickstarter.leku.tracker.LocationPickerTracker +import com.multiplatformkickstarter.leku.tracker.TrackEvents +import java.util.UUID + +private const val DEMO_LATITUDE = 41.4036299 +private const val DEMO_LONGITUDE = 2.1743558 +private const val POI1_LATITUDE = 41.4036339 +private const val POI1_LONGITUDE = 2.1721618 +private const val POI2_LATITUDE = 41.4023265 +private const val POI2_LONGITUDE = 2.1741417 class MainActivity : ComponentActivity() { + val lekuActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + onResult(result.data) + } else { + Log.d("RESULT****", "CANCELLED") + } + } + val mapPoisActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + onResultWithPois(result.data) + } else { + Log.d("RESULT WITH POIS****", "CANCELLED") + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build() + ) + StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()) + setContent { - MyApplicationTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - GreetingView(Greeting().greet()) - } - } + MainView() + } + + initializeLocationPickerTracker() + } + + private fun onResult(data: Intent?) { + Log.d("RESULT****", "OK") + val latitude = data?.getDoubleExtra(LATITUDE, 0.0) + Log.d("LATITUDE****", latitude.toString()) + val longitude = data?.getDoubleExtra(LONGITUDE, 0.0) + Log.d("LONGITUDE****", longitude.toString()) + val address = data?.getStringExtra(LOCATION_ADDRESS) + Log.d("ADDRESS****", address.toString()) + val postalcode = data?.getStringExtra(ZIPCODE) + Log.d("POSTALCODE****", postalcode.toString()) + val bundle = data?.getBundleExtra(TRANSITION_BUNDLE) + Log.d("BUNDLE TEXT****", bundle?.getString("test").toString()) + val fullAddress = data?.getParcelableExtra
(ADDRESS) + if (fullAddress != null) { + Log.d("FULL ADDRESS****", fullAddress.toString()) + } + val timeZoneId = data?.getStringExtra(TIME_ZONE_ID) + if (timeZoneId != null) { + Log.d("TIME ZONE ID****", timeZoneId) + } + val timeZoneDisplayName = data?.getStringExtra(TIME_ZONE_DISPLAY_NAME) + if (timeZoneDisplayName != null) { + Log.d("TIME ZONE NAME****", timeZoneDisplayName) + } + } + + private fun onResultWithPois(data: Intent?) { + Log.d("RESULT WITH POIS****", "OK") + val latitude = data?.getDoubleExtra(LATITUDE, 0.0) + Log.d("LATITUDE****", latitude.toString()) + val longitude = data?.getDoubleExtra(LONGITUDE, 0.0) + Log.d("LONGITUDE****", longitude.toString()) + val address = data?.getStringExtra(LOCATION_ADDRESS) + Log.d("ADDRESS****", address.toString()) + val lekuPoi = data?.getParcelableExtra(LEKU_POI) + Log.d("LekuPoi****", lekuPoi.toString()) + } + + private fun initializeLocationPickerTracker() { + LocationPicker.setTracker(MyPickerTracker(this)) + } + + private class MyPickerTracker(private val context: Context) : LocationPickerTracker { + override fun onEventTracked(event: TrackEvents) { + Toast.makeText(context, "Event: " + event.eventName, Toast.LENGTH_SHORT).show() } } } -@Composable -fun GreetingView(text: String) { - Text(text = text) +private fun onLaunchMapPickerClicked(context: Context) { + val activity = context as MainActivity + val locationPickerIntent = LocationPickerActivity.Builder(activity) + .withLocation(DEMO_LATITUDE, DEMO_LONGITUDE) + // .withGeolocApiKey("") + // .withGooglePlacesApiKey("") + .withSearchZone("es_ES") + // .withSearchZone(SearchZoneRect(LatLng(26.525467, -18.910366), LatLng(43.906271, 5.394197))) + .withDefaultLocaleSearchZone() + // .setCurrentLocation(BitmapDescriptorFactory.fromResource(R.drawable.common_full_open_on_phone)) + .setOtherLocation(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE)) + // .shouldReturnOkOnBackPressed() + // .withStreetHidden() + // .withCityHidden() + // .withZipCodeHidden() + // .withSatelliteViewHidden() + .withGoogleTimeZoneEnabled() + // .withVoiceSearchHidden() + .withUnnamedRoadHidden() + // .withSearchBarHidden() + .build() + + // this is optional if you want to return RESULT_OK if you don't set the + // latitude/longitude and click back button + locationPickerIntent.putExtra("test", "this is a test") + + activity.lekuActivityResultLauncher.launch(locationPickerIntent) +} + +private fun onLegacyMapClicked(context: Context) { + val activity = context as MainActivity + val locationPickerIntent = LocationPickerActivity.Builder(activity) + .withLocation(DEMO_LATITUDE, DEMO_LONGITUDE) + .withUnnamedRoadHidden() + .withLegacyLayout() + .build() + activity.lekuActivityResultLauncher.launch(locationPickerIntent) +} + +private val lekuPois: List + get() { + val pois = ArrayList() + + val locationPoi1 = Location("leku") + locationPoi1.latitude = POI1_LATITUDE + locationPoi1.longitude = POI1_LONGITUDE + val poi1 = LekuPoi(UUID.randomUUID().toString(), "Los bellota", locationPoi1) + pois.add(poi1) + + val locationPoi2 = Location("leku") + locationPoi2.latitude = POI2_LATITUDE + locationPoi2.longitude = POI2_LONGITUDE + val poi2 = LekuPoi(UUID.randomUUID().toString(), "Starbucks", locationPoi2) + poi2.address = "Plaça de la Sagrada Família, 19, 08013 Barcelona" + pois.add(poi2) + + return pois + } + +private fun onMapPoisClicked(context: Context) { + val activity = context as MainActivity + val locationPickerIntent = LocationPickerActivity.Builder(activity) + .withLocation(DEMO_LATITUDE, DEMO_LONGITUDE) + .withPois(lekuPois) + .build() + + activity.mapPoisActivityResultLauncher.launch(locationPickerIntent) +} + +private fun onMapWithStylesClicked(context: Context) { + val activity = context as MainActivity + val locationPickerIntent = LocationPickerActivity.Builder(activity) + .withLocation(DEMO_LATITUDE, DEMO_LONGITUDE) + .withMapStyle(R.raw.map_style_retro) + .build() + activity.mapPoisActivityResultLauncher.launch(locationPickerIntent) } -@Preview @Composable -fun DefaultPreview() { - MyApplicationTheme { - GreetingView("Hello, Android!") +@Preview(showBackground = true) +fun MainView() { + val context = LocalContext.current + + Column( + Modifier + .padding(16.dp, 40.dp, 16.dp, 0.dp) + .fillMaxSize(), + ) { + Image( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + painter = painterResource(id = R.mipmap.leku_img_logo), + contentDescription = null + ) + Text( + "MULTIPLATFORM", + Modifier + .padding(0.dp, 0.dp, 0.dp, 8.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center, + //fontSize = 24 + ) + + Spacer(Modifier.size(24.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color(context.resources.getColor(R.color.leku_app_blue)), + contentColor = Color.White + ), + onClick = { + onLaunchMapPickerClicked(context) + } + ) { + Text( + stringResource(id = R.string.launch_map_picker), + Modifier + .padding(8.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color(context.resources.getColor(R.color.leku_app_blue)), + contentColor = Color.White + ), + onClick = { + onLegacyMapClicked(context) + } + ) { + Text( + stringResource(id = R.string.launch_legacy_map_picker), + Modifier + .padding(8.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color(context.resources.getColor(R.color.leku_app_blue)), + contentColor = Color.White + ), + onClick = { + onMapWithStylesClicked(context) + } + ) { + Text( + stringResource(id = R.string.launch_map_picker_with_style), + Modifier + .padding(8.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color(context.resources.getColor(R.color.leku_app_blue)), + contentColor = Color.White + ), + onClick = { + onMapPoisClicked(context) + } + ) { + Text( + stringResource(id = R.string.launch_map_picker_with_pois), + Modifier + .padding(8.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + Box(modifier = Modifier.fillMaxSize()) { + Column( + Modifier.align(Alignment.BottomCenter), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + stringResource(id = R.string.leku_lib_version), + modifier = Modifier + + .padding(0.dp, 0.dp, 0.dp, 8.dp), + textAlign = TextAlign.Center + ) + Text( + "Developed by Multiplatform Kickstarter", + modifier = Modifier + .padding(0.dp, 0.dp, 0.dp, 8.dp), + textAlign = TextAlign.Center + ) + } + } + } } } diff --git a/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/SampleApplication.kt b/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/SampleApplication.kt new file mode 100644 index 0000000..e688eec --- /dev/null +++ b/androidApp/src/main/java/com/multiplatformkickstarter/leku/android/SampleApplication.kt @@ -0,0 +1,5 @@ +package com.multiplatformkickstarter.leku.android + +import androidx.multidex.MultiDexApplication + +class SampleApplication : MultiDexApplication() diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a62b9dd Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-hdpi/leku_img_logo.png b/androidApp/src/main/res/mipmap-hdpi/leku_img_logo.png new file mode 100644 index 0000000..b15e009 Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/leku_img_logo.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..19448f8 Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-mdpi/leku_img_logo.png b/androidApp/src/main/res/mipmap-mdpi/leku_img_logo.png new file mode 100644 index 0000000..d97b7eb Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/leku_img_logo.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9c73424 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xhdpi/leku_img_logo.png b/androidApp/src/main/res/mipmap-xhdpi/leku_img_logo.png new file mode 100644 index 0000000..a0d13b2 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/leku_img_logo.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..e81c519 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxhdpi/leku_img_logo.png b/androidApp/src/main/res/mipmap-xxhdpi/leku_img_logo.png new file mode 100644 index 0000000..c2d9c28 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/leku_img_logo.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..fcc3044 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/leku_img_logo.png b/androidApp/src/main/res/mipmap-xxxhdpi/leku_img_logo.png new file mode 100644 index 0000000..26b8b78 Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/leku_img_logo.png differ diff --git a/androidApp/src/main/res/raw/map_style_retro.json b/androidApp/src/main/res/raw/map_style_retro.json new file mode 100644 index 0000000..2ecd6e2 --- /dev/null +++ b/androidApp/src/main/res/raw/map_style_retro.json @@ -0,0 +1,215 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#ebe3cd" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#523735" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#f5f1e6" + } + ] + }, + { + "featureType": "administrative", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#c9b2a6" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#dcd2be" + } + ] + }, + { + "featureType": "administrative.land_parcel", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#ae9e90" + } + ] + }, + { + "featureType": "landscape.natural", + "elementType": "geometry", + "stylers": [ + { + "color": "#dfd2ae" + } + ] + }, + { + "featureType": "poi", + "elementType": "geometry", + "stylers": [ + { + "color": "#dfd2ae" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#93817c" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry.fill", + "stylers": [ + { + "color": "#a5b076" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#447530" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#f5f1e6" + } + ] + }, + { + "featureType": "road.arterial", + "elementType": "geometry", + "stylers": [ + { + "color": "#fdfcf8" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#f8c967" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#e9bc62" + } + ] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry", + "stylers": [ + { + "color": "#e98d58" + } + ] + }, + { + "featureType": "road.highway.controlled_access", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#db8555" + } + ] + }, + { + "featureType": "road.local", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#806b63" + } + ] + }, + { + "featureType": "transit.line", + "elementType": "geometry", + "stylers": [ + { + "color": "#dfd2ae" + } + ] + }, + { + "featureType": "transit.line", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#8f7d77" + } + ] + }, + { + "featureType": "transit.line", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#ebe3cd" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "geometry", + "stylers": [ + { + "color": "#dfd2ae" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry.fill", + "stylers": [ + { + "color": "#b9d3c2" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#92998d" + } + ] + } +] \ No newline at end of file diff --git a/androidApp/src/main/res/values/colors.xml b/androidApp/src/main/res/values/colors.xml new file mode 100644 index 0000000..7c685f1 --- /dev/null +++ b/androidApp/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #3DA8E1 + #053B58 + + + + + diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000..e5406ec --- /dev/null +++ b/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + Leku Demo + + LAUNCH MAP LOCATION PICKER ACTIVITY + LAUNCH LEGACY MAP LOCATION ACTIVITY + LAUNCH MAP WITH POIS + LAUNCH MAP WITH STYLE + version 1.0.0 + diff --git a/androidApp/src/main/res/values/styles.xml b/androidApp/src/main/res/values/styles.xml index 6b4fa3d..e9d4ecc 100644 --- a/androidApp/src/main/res/values/styles.xml +++ b/androidApp/src/main/res/values/styles.xml @@ -1,3 +1,17 @@ - + + + + diff --git a/build.gradle.kts b/build.gradle.kts index d24ef08..5685f2d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,5 +4,6 @@ plugins { alias(libs.plugins.androidLibrary).apply(false) alias(libs.plugins.kotlinAndroid).apply(false) alias(libs.plugins.kotlinMultiplatform).apply(false) + alias(libs.plugins.compose.multiplatform).apply(false) alias(libs.plugins.compose.compiler).apply(false) } diff --git a/gradle.properties b/gradle.properties index 7f53ad4..77918fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,4 +8,6 @@ kotlin.code.style=official #Android android.useAndroidX=true -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true + +libVersion=1.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5aac08b..40b8431 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,39 @@ [versions] +composeMultiplatform = "1.7.3" +activityCompose = "1.10.1" agp = "8.9.1" -kotlin = "2.0.0" -compose = "1.5.4" -compose-material3 = "1.1.2" -androidx-activityCompose = "1.8.0" +fragmentKtx = "1.8.6" +googleMapsServices = "0.2.9" +kotlin = "2.1.10" +kotlinxCoroutinesGuava = "1.9.0" +lifecycleViewmodelCompose = "2.8.7" +material = "1.12.0" +multidex = "2.0.1" +places = "4.2.0" +playServicesLocation = "21.3.0" +playServicesMaps = "19.2.0" +runtimeAndroid = "1.7.8" [libraries] + +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +google-maps-services = { module = "com.google.maps:google-maps-services", version.ref = "googleMapsServices" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } -compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } -compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } -compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-material3 = { module = "androidx.compose.material3:material3" } +kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } +places = { module = "com.google.android.libraries.places:places", version.ref = "places" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } +play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } + +# Android +google-material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -20,4 +41,5 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5210d36..f3e6b76 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,14 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://androidx.dev/storage/compose-compiler/repository") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap") + maven("https://s01.oss.sonatype.org/content/repositories/releases/") + } + + versionCatalogs { + create("libs") } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index a6634d1..843f372 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) + id("org.jetbrains.compose") + alias(libs.plugins.compose.compiler) } kotlin { @@ -10,7 +12,7 @@ kotlin { compilations.all { compileTaskProvider.configure { compilerOptions { - jvmTarget.set(JvmTarget.JVM_1_8) + jvmTarget.set(JvmTarget.JVM_21) } } } @@ -28,8 +30,37 @@ kotlin { } sourceSets { + all { + languageSettings { + optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") + optIn("androidx.compose.foundation.layout.ExperimentalLayoutApi") + optIn("androidx.compose.material3.ExperimentalMaterial3Api") + } + } + androidMain.dependencies { + implementation(libs.androidx.runtime.android) + implementation(libs.androidx.fragment.ktx) + implementation(libs.kotlinx.coroutines.guava) + + // Legacy material library used for Theme.MaterialComponent in XML + implementation(libs.google.material) + + implementation(libs.play.services.maps) + implementation(libs.play.services.location) + implementation(libs.places) + + implementation(libs.google.maps.services) + } commonMain.dependencies { - //put your multiplatform dependencies here + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.animation) + implementation(compose.materialIconsExtended) + implementation(compose.components.resources) + } + iosMain.dependencies { } commonTest.dependencies { implementation(libs.kotlin.test) @@ -44,7 +75,7 @@ android { minSdk = 28 } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } } diff --git a/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/AddressExt.kt b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/AddressExt.kt new file mode 100644 index 0000000..4dc3936 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/AddressExt.kt @@ -0,0 +1,27 @@ +package com.multiplatformkickstarter.leku + +import android.content.Context +import android.location.Address + +fun Address.getFullAddressString(context: Context): String { + if (featureName == null) { + return context.getString(R.string.leku_unknown_location) + } + var fullAddress = getAddressLine(0) + if (fullAddress.isNullOrEmpty()) { + fullAddress = "" + featureName?.let { + fullAddress += it + } + if (subLocality != null && subLocality.isNotEmpty()) { + fullAddress += ", $subLocality" + } + if (locality != null && locality.isNotEmpty()) { + fullAddress += ", $locality" + } + if (countryName != null && countryName.isNotEmpty()) { + fullAddress += ", $countryName" + } + } + return fullAddress +} diff --git a/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuPoi.kt b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuPoi.kt new file mode 100644 index 0000000..89fb754 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuPoi.kt @@ -0,0 +1,55 @@ +package com.multiplatformkickstarter.leku + +import android.location.Location +import android.os.Parcel +import android.os.Parcelable + +class LekuPoi : Parcelable { + var id: String + var location: Location + var title: String + var address: String = "" + + constructor(id: String, title: String, location: Location) { + this.id = id + this.location = location + this.title = title + } + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(this.id) + dest.writeParcelable(this.location, flags) + dest.writeString(this.title) + dest.writeString(this.address) + } + + private constructor(`in`: Parcel) { + this.id = `in`.readString()!! + this.location = `in`.readParcelable(Location::class.java.classLoader)!! + this.title = `in`.readString()!! + this.address = `in`.readString()!! + } + + override fun toString(): String { + return "LekuPoi{" + "id='" + id + '\''.toString() + ", location=" + location + ", title='" + + title + '\''.toString() + ", address='" + address + '\''.toString() + '}'.toString() + } + + companion object { + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): LekuPoi { + return LekuPoi(source) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} diff --git a/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuViewHolder.kt b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuViewHolder.kt new file mode 100644 index 0000000..b3d98f6 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LekuViewHolder.kt @@ -0,0 +1,8 @@ +package com.multiplatformkickstarter.leku + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class LekuViewHolder( + view: View +) : RecyclerView.ViewHolder(view) diff --git a/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPicker.kt b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPicker.kt new file mode 100644 index 0000000..f4f8c31 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPicker.kt @@ -0,0 +1,30 @@ +package com.multiplatformkickstarter.leku + +import com.multiplatformkickstarter.leku.tracker.LocationPickerTracker +import com.multiplatformkickstarter.leku.tracker.TrackEvents + +object LocationPicker { + + private val EMPTY_TRACKER = EmptyLocationPickerTracker() + + private var tracker: LocationPickerTracker = EMPTY_TRACKER + + fun setTracker(tracker: LocationPickerTracker?) { + if (tracker == null) { + throw IllegalArgumentException("The LocationPickerTracker instance can't be null.") + } + LocationPicker.tracker = tracker + } + + fun getTracker(): LocationPickerTracker { + return tracker + } + + fun reset() { + tracker = EMPTY_TRACKER + } + + class EmptyLocationPickerTracker : LocationPickerTracker { + override fun onEventTracked(event: TrackEvents) { } + } +} diff --git a/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPickerActivity.kt b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPickerActivity.kt new file mode 100644 index 0000000..a2f4dcb --- /dev/null +++ b/shared/src/androidMain/kotlin/com/multiplatformkickstarter/leku/LocationPickerActivity.kt @@ -0,0 +1,1895 @@ +package com.multiplatformkickstarter.leku + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.content.res.Resources +import android.graphics.PorterDuff +import android.location.Address +import android.location.Geocoder +import android.location.Location +import android.os.Build +import android.os.Bundle +import android.speech.RecognizerIntent +import android.text.Editable +import android.text.TextWatcher +import android.util.TypedValue +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ListView +import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RawRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ComponentActivity +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.multiplatformkickstarter.leku.geocoder.AndroidGeocoderDataSource +import com.multiplatformkickstarter.leku.geocoder.GeocoderDataSourceInterface +import com.multiplatformkickstarter.leku.geocoder.GeocoderPresenter +import com.multiplatformkickstarter.leku.geocoder.GeocoderRepository +import com.multiplatformkickstarter.leku.geocoder.GeocoderViewInterface +import com.multiplatformkickstarter.leku.geocoder.GoogleGeocoderDataSource +import com.multiplatformkickstarter.leku.geocoder.PlaceSuggestion +import com.multiplatformkickstarter.leku.geocoder.adapters.DefaultAddressAdapter +import com.multiplatformkickstarter.leku.geocoder.adapters.DefaultSuggestionAdapter +import com.multiplatformkickstarter.leku.geocoder.adapters.SearchViewHolder +import com.multiplatformkickstarter.leku.geocoder.adapters.SuggestionViewHolder +import com.multiplatformkickstarter.leku.geocoder.adapters.base.LekuSearchAdapter +import com.multiplatformkickstarter.leku.geocoder.api.AddressBuilder +import com.multiplatformkickstarter.leku.geocoder.api.NetworkClient +import com.multiplatformkickstarter.leku.geocoder.api.SuggestionBuilder +import com.multiplatformkickstarter.leku.geocoder.places.GooglePlacesDataSource +import com.multiplatformkickstarter.leku.geocoder.timezone.GoogleTimeZoneDataSource +import com.multiplatformkickstarter.leku.locale.DefaultCountryLocaleRect +import com.multiplatformkickstarter.leku.locale.SearchZoneRect +import com.multiplatformkickstarter.leku.permissions.PermissionUtils +import com.multiplatformkickstarter.leku.tracker.TrackEvents +import com.multiplatformkickstarter.leku.utils.ReactiveLocationProvider +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.common.api.GoogleApiClient +import com.google.android.gms.location.LocationListener +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.GoogleMap.MAP_TYPE_NORMAL +import com.google.android.gms.maps.GoogleMap.MAP_TYPE_SATELLITE +import com.google.android.gms.maps.MapsInitializer +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MapStyleOptions +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.libraries.places.api.Places +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.maps.GeoApiContext +import java.util.Locale +import java.util.TimeZone +import kotlin.collections.set + +const val LATITUDE = "latitude" +const val LONGITUDE = "longitude" +const val ZIPCODE = "zipcode" +const val ADDRESS = "address" +const val LOCATION_ADDRESS = "location_address" +const val TRANSITION_BUNDLE = "transition_bundle" +const val LAYOUTS_TO_HIDE = "layouts_to_hide" +const val SEARCH_ZONE = "search_zone" +const val SEARCH_ZONE_RECT = "search_zone_rect" +const val SEARCH_ZONE_DEFAULT_LOCALE = "search_zone_default_locale" +const val BACK_PRESSED_RETURN_OK = "back_pressed_return_ok" +const val ENABLE_SATELLITE_VIEW = "enable_satellite_view" +const val ENABLE_LOCATION_PERMISSION_REQUEST = "enable_location_permission_request" +const val ENABLE_GOOGLE_PLACES = "enable_google_places" +const val ENABLE_GOOGLE_TIME_ZONE = "enable_google_time_zone" +const val POIS_LIST = "pois_list" +const val LEKU_POI = "leku_poi" +const val ENABLE_VOICE_SEARCH = "enable_voice_search" +const val TIME_ZONE_ID = "time_zone_id" +const val TIME_ZONE_DISPLAY_NAME = "time_zone_display_name" +const val MAP_STYLE = "map_style" +const val UNNAMED_ROAD_VISIBILITY = "unnamed_road_visibility" +const val WITH_LEGACY_LAYOUT = "with_legacy_layout" +const val SEARCH_BAR_HIDDEN = "search_view_hidden" +private const val GEOLOC_API_KEY = "geoloc_api_key" +private const val PLACES_API_KEY = "places_api_key" +private const val LOCATION_KEY = "location_key" +private const val LAST_LOCATION_QUERY = "last_location_query" +private const val OPTIONS_HIDE_STREET = "street" +private const val OPTIONS_HIDE_CITY = "city" +private const val OPTIONS_HIDE_ZIPCODE = "zipcode" +private const val UNNAMED_ROAD_WITH_COMMA = "Unnamed Road, " +private const val UNNAMED_ROAD_WITH_HYPHEN = "Unnamed Road - " +private const val CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000 +private const val DEFAULT_ZOOM = 16 +private const val WIDER_ZOOM = 6 +private const val MIN_CHARACTERS = 2 +private const val DEBOUNCE_TIME = 400 +private const val PADDING_GOOGLE_LOGO_TOP_RIGHT = 24.0f + +class LocationPickerActivity : + AppCompatActivity(), + OnMapReadyCallback, + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener, + LocationListener, + GoogleMap.OnMapLongClickListener, + GeocoderViewInterface, + GoogleMap.OnMapClickListener { + + companion object { + var customDataSource: GeocoderDataSourceInterface? = null + var customAdapter: LekuSearchAdapter<*, *>? = null + var currentLocationBitmapMaker: BitmapDescriptor? = null + var otherLocationBitmapMaker: BitmapDescriptor? = null + } + + private var map: GoogleMap? = null + private var googleApiClient: GoogleApiClient? = null + private var currentLocation: Location? = null + private var currentLekuPoi: LekuPoi? = null + private var geocoderPresenter: GeocoderPresenter? = null + + private var adapter: ArrayAdapter? = null + private var searchView: EditText? = null + private var street: TextView? = null + private var coordinates: TextView? = null + private var longitude: TextView? = null + private var latitude: TextView? = null + private var city: TextView? = null + private var zipCode: TextView? = null + private var locationInfoLayout: FrameLayout? = null + private var progressBar: ProgressBar? = null + private var listResult: ListView? = null + private var searchResultsList: RecyclerView? = null + private var searchAdapter: LekuSearchAdapter<*, *>? = null + private lateinit var linearLayoutManager: RecyclerView.LayoutManager + private var clearSearchButton: ImageView? = null + private var searchOption: MenuItem? = null + private var clearLocationButton: ImageButton? = null + private var searchEditLayout: LinearLayout? = null + private var searchFrameLayout: FrameLayout? = null + private var suggestionsToast: Toast? = null + private var locationsToast: Toast? = null + + private val locationList = ArrayList
() + private val suggestionList = ArrayList() + private var locationNameList: MutableList = ArrayList() + private var hasWiderZoom = false + private val bundle = Bundle() + private var selectedAddress: Address? = null + private var selectedSuggestion: PlaceSuggestion? = null + private var isLocationInformedFromBundle = false + private var isStreetVisible = true + private var isCityVisible = true + private var isZipCodeVisible = true + private var shouldReturnOkOnBackPressed = false + private var enableSatelliteView = true + private var enableLocationPermissionRequest = true + private var geoApiKey: String? = null + private var googlePlacesApiKey: String? = null + private var isGoogleTimeZoneEnabled = false + private var searchZone: String? = null + private var searchZoneRect: SearchZoneRect? = null + private var isSearchZoneWithDefaultLocale = false + private var poisList: List? = null + private var lekuPoisMarkersMap: MutableMap? = null + private var currentMarker: Marker? = null + private var textWatcher: TextWatcher? = null + private var googleGeocoderDataSource: GoogleGeocoderDataSource? = null + private var isVoiceSearchEnabled = true + private var isUnnamedRoadVisible = true + private var mapStyle: Int? = null + private var isLegacyLayoutEnabled = false + private var isSearchLayoutShown = false + private var isSearchBarHidden = false + private var placeResolution = false + + private lateinit var toolbar: MaterialToolbar + private lateinit var timeZone: TimeZone + + private val voiceRecognitionActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + onVoiceRecognitionActivityResult(result.data) + } + } + + private val defaultZoom: Int + get() { + return if (hasWiderZoom) { + WIDER_ZOOM + } else { + DEFAULT_ZOOM + } + } + + private val locationAddress: String + get() { + var locationAddress = "" + street?.let { + if (it.text.toString().isNotEmpty()) { + locationAddress = if (isUnnamedRoadVisible) { + it.text.toString() + } else { + removeUnnamedRoad(it.text.toString()) + } + } + } + city?.let { + if (it.text.toString().isNotEmpty()) { + if (locationAddress.isNotEmpty()) { + locationAddress += ", " + } + locationAddress += it.text.toString() + } + } + return locationAddress + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + updateValuesFromBundle(savedInstanceState) + setUpContentView() + setUpMainVariables() + setUpResultsList() + setUpToolBar() + checkLocationPermission() + setUpSearchView() + setUpMapIfNeeded() + setUpFloatingButtons() + buildGoogleApiClient() + track(TrackEvents.ON_LOAD_LOCATION_PICKER) + } + + private fun setUpContentView() { + if (isLegacyLayoutEnabled) { + setContentView(R.layout.leku_activity_location_picker_legacy) + } else { + window.setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + var flags: Int = window.decorView.systemUiVisibility + flags = flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + window.decorView.systemUiVisibility = flags + } + + setContentView(R.layout.leku_activity_location_picker) + moveGoogleLogoToTopRight() + } + } + + @SuppressLint("InlinedApi") + private fun moveGoogleLogoToTopRight() { + val contentView: View = findViewById(android.R.id.content) + val googleLogo: View? = contentView.findViewWithTag("GoogleWatermark") + googleLogo?.let { + val glLayoutParams: RelativeLayout.LayoutParams = + it.layoutParams as RelativeLayout.LayoutParams + glLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0) + glLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0) + glLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_START, 0) + glLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE) + glLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END, RelativeLayout.TRUE) + val paddingTopInPixels = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + PADDING_GOOGLE_LOGO_TOP_RIGHT, + resources.displayMetrics + ).toInt() + it.setPadding(0, paddingTopInPixels, 0, 0) + it.layoutParams = glLayoutParams + } + } + + private fun checkLocationPermission() { + if (enableLocationPermissionRequest && + PermissionUtils.shouldRequestLocationStoragePermission(applicationContext) + ) { + PermissionUtils.requestLocationPermission(this) + } + } + + private fun track(event: TrackEvents) { + LocationPicker.getTracker().onEventTracked(event) + } + + private fun setUpMainVariables() { + var placesDataSource: GooglePlacesDataSource? = null + if (!Places.isInitialized() && !googlePlacesApiKey.isNullOrEmpty()) { + googlePlacesApiKey?.let { + Places.initialize(applicationContext, it) + } + placesDataSource = GooglePlacesDataSource(Places.createClient(this)) + } + val geocoder = Geocoder(this, Locale.getDefault()) + if (googleGeocoderDataSource == null) { + googleGeocoderDataSource = GoogleGeocoderDataSource( + NetworkClient(), + AddressBuilder(), + SuggestionBuilder() + ) + } + val geocoderRepository = GeocoderRepository( + customDataSource, + AndroidGeocoderDataSource(geocoder), + googleGeocoderDataSource!! + ) + val timeZoneDataSource = GoogleTimeZoneDataSource( + GeoApiContext.Builder().apiKey(GoogleTimeZoneDataSource.getGeoApiKey(this)).build() + ) + geocoderPresenter = GeocoderPresenter( + ReactiveLocationProvider(applicationContext), + geocoderRepository, + placesDataSource, + timeZoneDataSource + ) + geocoderPresenter?.setUI(this) + progressBar = findViewById(R.id.loading_progress_bar) + progressBar?.visibility = View.GONE + locationInfoLayout = findViewById(R.id.location_info) + longitude = findViewById(R.id.longitude) + latitude = findViewById(R.id.latitude) + street = findViewById(R.id.street) + coordinates = findViewById(R.id.coordinates) + city = findViewById(R.id.city) + zipCode = findViewById(R.id.zipCode) + clearSearchButton = findViewById(R.id.leku_clear_search_image) + clearSearchButton?.setOnClickListener { + searchView?.setText("") + } + locationNameList = ArrayList() + clearLocationButton = findViewById(R.id.btnClearSelectedLocation) + clearLocationButton?.setOnClickListener { + currentLocation = null + currentLekuPoi = null + currentMarker?.remove() + changeLocationInfoLayoutVisibility(View.GONE) + } + searchEditLayout = findViewById(R.id.leku_search_touch_zone) + searchFrameLayout = findViewById(R.id.search_frame_layout) + + currentLocationBitmapMaker = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED) + otherLocationBitmapMaker = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE) + } + + private fun setUpResultsList() { + if (isLegacyLayoutEnabled) { + listResult = findViewById(R.id.resultlist) + adapter = + ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, locationNameList) + listResult?.let { + it.adapter = adapter + it.setOnItemClickListener { _, _, position, _ -> + if (locationList[position].hasLatitude() && + locationList[position].hasLongitude() + ) { + setNewLocation(locationList[position]) + changeListResultVisibility(View.GONE) + closeKeyboard() + } + } + } + } else { + linearLayoutManager = LinearLayoutManager(this) + when { + placeResolution -> { + searchAdapter = customAdapter ?: DefaultSuggestionAdapter( + this, + ) + searchAdapter?.onClick = { + if (suggestionList.size > it) { + setNewSuggestion(suggestionList[it]) + changeListResultVisibility(View.GONE) + closeKeyboard() + hideSearchLayout() + } + } + } + else -> { + searchAdapter = customAdapter ?: DefaultAddressAdapter( + this, + ) + searchAdapter?.onClick = { + if (locationList[it].hasLatitude() && locationList[it].hasLongitude()) { + setNewLocation(locationList[it]) + changeListResultVisibility(View.GONE) + closeKeyboard() + hideSearchLayout() + } + } + } + } + searchResultsList = findViewById(R.id.search_result_list).apply { + setHasFixedSize(true) + layoutManager = linearLayoutManager + adapter = searchAdapter + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + } + } + + private fun setUpToolBar() { + toolbar = findViewById(R.id.map_search_toolbar) + setSupportActionBar(toolbar) + supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + it.setDisplayShowTitleEnabled(false) + } + } + + private fun switchToolbarVisibility() { + if (isPlayServicesAvailable()) { + toolbar.visibility = View.VISIBLE + } else { + toolbar.visibility = View.GONE + } + } + + private fun setUpSearchView() { + searchView = findViewById(R.id.leku_search) + if (isSearchBarHidden) { + searchEditLayout?.visibility = View.GONE + } else { + searchView?.setOnEditorActionListener { v, actionId, _ -> + var handled = false + if (actionId == EditorInfo.IME_ACTION_SEARCH && v.text.toString().isNotEmpty()) { + retrieveLocationFrom(v.text.toString()) + closeKeyboard() + handled = true + } + handled + } + createSearchTextChangeObserver() + if (!isLegacyLayoutEnabled) { + searchView?.setOnFocusChangeListener { _: View?, hasFocus: Boolean -> + if (hasFocus) { + showSearchLayout() + } + } + } + } + } + + private fun createSearchTextChangeObserver() { + searchView?.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable) {} + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + onSearchTextChanged(s.toString()) + } + }) + } + + private fun onSearchTextChanged(term: String) { + if (term.isEmpty()) { + if (isLegacyLayoutEnabled) { + adapter?.let { + it.clear() + it.notifyDataSetChanged() + } + } else { + searchAdapter?.notifyDataSetChanged() + } + showLocationInfoLayout() + clearSearchButton?.visibility = View.INVISIBLE + searchOption?.setIcon(R.drawable.leku_ic_mic_legacy) + updateVoiceSearchVisibility() + } else { + if (term.length > MIN_CHARACTERS) { + retrieveLocationWithDebounceTimeFrom(term) + } + clearSearchButton?.visibility = View.VISIBLE + searchOption?.setIcon(R.drawable.leku_ic_search) + searchOption?.isVisible = true + } + } + + private fun showSearchLayout() { + searchFrameLayout?.setBackgroundResource(R.color.leku_white) + searchEditLayout?.setBackgroundResource(R.drawable.leku_search_text_with_border_background) + searchResultsList?.visibility = View.VISIBLE + isSearchLayoutShown = true + } + + private fun hideSearchLayout() { + searchFrameLayout?.setBackgroundResource(android.R.color.transparent) + searchEditLayout?.setBackgroundResource(R.drawable.leku_search_text_background) + searchResultsList?.visibility = View.GONE + searchView?.clearFocus() + isSearchLayoutShown = false + } + + private fun setUpFloatingButtons() { + val btnMyLocation = findViewById(R.id.btnMyLocation) + btnMyLocation.setOnClickListener { + checkLocationPermission() + geocoderPresenter?.getLastKnownLocation() + track(TrackEvents.ON_LOCALIZED_ME) + } + + val btnAcceptLocation = if (isLegacyLayoutEnabled) { + findViewById(R.id.btnAccept) + } else { + findViewById