7

Google maps now offers a way to "share a place" with what appears to be a predefined list of sources. When users search for a place on Google Maps, whether it's a specific address, cross-street, or restaurant name, there's a new button called "share this place" that posts the location info to Google Buzz, Facebook, Twitter, or via e-mail or SMS. I would like to either have my application included in this list or determine how to obtain the lat/lon of that selected location. Does anyone have any ideas?

stanlick
  • 1,442
  • 3
  • 16
  • 29

2 Answers2

9

I figured it out. Here's a sample manifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.yourapp" android:versionCode="1"
    android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".YourApp" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND"></action>
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>
    </application>
    <uses-sdk android:minSdkVersion="4" />
</manifest> 

Simply add the SEND intent filter just as it is above to your activity. The "share this place" simply performs a "SEND" intent with the mime type of "text/plain". If you register an intent filter for that type, then your app will show up in the list. Mine did. =)

HenryAdamsJr
  • 850
  • 2
  • 9
  • 21
  • Thanks Henry! I will give this a try as soon as the alligators allow. – stanlick Sep 21 '10 at 14:38
  • Sweet, that worked great! Now then, how is it working? Does my app have to be running to show up in the map's "share this place?" I don't see the connection between the two. Also, do I need a broadcast receiver or some such to receive the call back when the user selects my app? – stanlick Sep 21 '10 at 23:33
  • The way I've shown you above tells Android that your app can receive a send intent of type "text/plain". Your app does not have to be running for it to be in the list because when you select it, Maps with actually launch the activity your intent filter is defined on. Then, you simply pull the text out of the context that's passed to the activity. – HenryAdamsJr Sep 27 '10 at 18:53
  • 2
    Is there any documentation concerning what I might expect to get back in the Bundle extras? I have experimented with a few places and what I get back appears to be nearly random! bundle.get("android.intent.extra.TEXT") returns a String where the variable number of lines are separated by a /n char. Also, some of the lines start off with a number (1,2,3) and others do not. I thought the 1 might signify phone number, however, some phone numbers returned were not preceded by the 1! Fishing random data out of a Bundle is crazy! – stanlick Sep 28 '10 at 23:08
  • 2
    I didn't find any documentation, and I doubt there is any. I just played around with it until I got what I needed. I just wanted to be able to pull out the GPS coordinates. Also, could you please accept my answer. =) – HenryAdamsJr Sep 29 '10 at 04:24
  • 4
    Did you figure out how to get the GPS coordinates? How? – maxpower47 Dec 20 '10 at 14:42
  • 1
    I would also want to know how you got the GPS coordinates from it. Google Maps just sends a shortened URL of the place in form of `goo.gl/maps/XyZ` – ercan Jan 10 '14 at 08:56
  • @HenryAdamsJr Please tell me how to get the gps coordinates from it i really need it :( – zyngot Jun 15 '20 at 22:54
  • This post is almost 10 years old. You'd be better off with a new question as I'm sure anything that worked 10 years ago is irrelevant now. – HenryAdamsJr Jun 22 '20 at 20:26
  • How can I do this for Expo? – Bassel Turky May 13 '22 at 19:53
0

Did you figure out how to get the GPS coordinates? How?

I would also want to know how you got the GPS coordinates from it.

Here is how you can try to get coordinates:

As @HenryAdamsJr said you need to declare an <intent-filter> first, then with the intent the app will get some text which looks like this:

 Letícia Bronzoni Fotografi
 +46 73 255 28 32
 https://maps.app.goo.gl/kijJBem2FGkj2S99A

As you can see there is an url which when is being invoked redirects to something like:

1. https://www.google.com/maps/place/Grodhavet,+Karlbergs+strand+12,+171+73+Solna,+Sweden/data=!4m2!3m1!1s0x465f9d8ec1ecc2c3:0xbc9f46e4d42faea4?utm_source=mstt_1&entry=gps&g_ep=IIgnKgA%3D&ucbcb=1

2. https://www.google.com/maps/place/Karlav%C3%A4gen+32,+114+31+Stockholm,+Sweden/data=!4m6!3m5!1s0x465f9d4383805daf:0xffd88b394b6ac3b8!7e2!8m2!3d59.33992!4d18.0729719?utm_source=mstt_1&entry=gps&g_ep=CAESCTExLjYwLjcwMxgAIIgnKgBCAlJV&ucbcb=1 

3. https://www.google.com/maps/place/59.360982,18.069179/data=!4m6!3m5!1s0!7e2!8m2!3d59.360981499999994!4d18.0691788?utm_source=mstt_1&entry=gps&g_ep=CAESCTExLjYwLjcwMxgAIIgnKgBCAlJV&ucbcb=1

Case 1. The worst one.
It happens when a POI was selected on GMaps. The resulting url has only a more detailed address and it needs to be looked up with a Geocoder or so (see example below).

Case 2. The best one!
It happens when a random point was selected but then GMaps converted it to an address. The resulting url has "data=....!3dXXXX!4dYYYY" part with coordinates.

Case 3. The one which is ok.
It happens when a random point was selected and GMaps could not figure out any address. The resulting url has "place/XXXX,YYYY" part with coordinates.

Here is an how I was handling it:


// Somewhere in Activity::onCreate or ::onNewIntent
override fun onCreate(savedInstanceState: Bundle?) {
    .....
    
    val linkHash = IncomingShareFromGoogleMaps.getGoogleMapsShareLinkHash(intent)
}


// Then somewhere in a ViewModel or so 
val shareHandler: IncomingShareFromGoogleMaps = ...
fun processShare(linkHash:String) {
    viewModelScope.launch {
      val coordinates = shareHandler.shortenedUrlToCoordinates(linkHash)
      // Profit!
    }
}



class IncomingShareFromGoogleMaps @Inject constructor(
    private val errorReporter: ErrorReporter,
    private val geocoder: Geocoder,
) {
    companion object {
        private const val TIMEOUT_CONNECT_MS = 5_000
        private const val TIMEOUT_WRITE_MS = 5_000
        private const val TIMEOUT_READ_MS = 5_000
        private const val GOOGLE_SHORT_URL_PREFIX = "https://maps.app.goo.gl/"


        fun getGoogleMapsShareLinkHash(intent: Intent): String? =
            intent
                .takeIf { it.action == "android.intent.action.SEND" }?.extras
                ?.getString("android.intent.extra.TEXT")
                ?.takeUnless { it.isBlank() }
                ?.split("\n")
                ?.lastOrNull()
                ?.takeIf { it.startsWith(GOOGLE_SHORT_URL_PREFIX) }
                ?.removePrefix(GOOGLE_SHORT_URL_PREFIX)
                
    }

    suspend fun shortenedUrlToCoordinates(shortenedUrl: String) = withContext(Dispatchers.IO) {
        val resultingUrl =
            createHttpClient()
                .performGetRequest(GOOGLE_SHORT_URL_PREFIX + shortenedUrl)
                .request.url.toString()

        return@withContext tryToGetCoordinates(resultingUrl)
    }

    /**
     * Takes a url which was returned by Google.Maps and tries to extract some coordinates.
     * Example of the `resultingUrl`:
     * https://www.google.com/maps/place/Grodhavet,+Karlbergs+strand+12,+171+73+Solna,+Sweden/data=!4m2!3m1!1s0x465f9d8ec1ecc2c3:0xbc9f46e4d42faea4?utm_source=mstt_1&entry=gps&g_ep=IIgnKgA%3D&ucbcb=1
     * https://www.google.com/maps/place/Karlav%C3%A4gen+32,+114+31+Stockholm,+Sweden/data=!4m6!3m5!1s0x465f9d4383805daf:0xffd88b394b6ac3b8!7e2!8m2!3d59.33992!4d18.0729719?utm_source=mstt_1&entry=gps&g_ep=CAESCTExLjYwLjcwMxgAIIgnKgBCAlJV&ucbcb=1
     * https://www.google.com/maps/place/59.360982,18.069179/data=!4m6!3m5!1s0!7e2!8m2!3d59.360981499999994!4d18.0691788?utm_source=mstt_1&entry=gps&g_ep=CAESCTExLjYwLjcwMxgAIIgnKgBCAlJV&ucbcb=1
     * So it tries first to take coordinates from "data=....!3dXXX!4dYYY" part of the url,
     * then from "place/XXX,YYY" part of the url,
     * then it tries to look up the address from "place/" part with Mapbox.Geocoder SDK.
     */
    private fun tryToGetCoordinates(resultingUrl: String) =
        tryToGetCoordinatesFromDataSection(resultingUrl)
            ?: tryToGetCoordinatesFromPlaceSection(resultingUrl)
            ?: tryToGetCoordinatesByAddress(resultingUrl)

    private fun tryToGetCoordinatesFromPlaceSection(resultingUrl: String) =
        try {
            """place/(\d+\.\d+),(\d+\.\d+)""".toRegex()
                .find(resultingUrl)?.groupValues
                ?.takeIf { it.size == 3 }
                ?.let { list ->
                    Coordinates(
                        longitude = list[2].toDouble(),
                        latitude = list[1].toDouble()
                    )
                }
        } catch (e: NumberFormatException) {
            errorReporter.reportError(e, "shortenedUrlToCoordinates, failed")
            null
        }

    private fun tryToGetCoordinatesFromDataSection(resultingUrl: String) =
        try {
            val lat = """data=\S*!3d(\d+\.\d+)""".toRegex()
                .find(resultingUrl)?.groupValues
                ?.takeIf { it.size > 1 }
                ?.get(1)
                ?.toDouble()

            val lng = """data=\S*!4d(\d+\.\d+)""".toRegex()
                .find(resultingUrl)?.groupValues
                ?.takeIf { it.size > 1 }
                ?.get(1)
                ?.toDouble()

            when {
                lat != null && lng != null -> Coordinates(latitude = lat, longitude = lng)
                else -> null
            }
        } catch (e: NumberFormatException) {
            errorReporter.reportError(e, "tryToGetCoordinatesFromDataSection, failed")
            null
        }

    private fun tryToGetCoordinatesByAddress(resultingUrl: String): Coordinates? =
        """place/(\S*)/data""".toRegex()
            .find(resultingUrl)?.groupValues
            ?.takeIf { it.size == 2 }?.get(1)
            ?.let { geocoder.getCoordinatesForAddress(URLDecoder.decode(it, "UTF-8")) }

    private fun createHttpClient() =
        OkHttpClient.Builder()
            .connectTimeout(TIMEOUT_CONNECT_MS.toLong(), TimeUnit.MILLISECONDS)
            .writeTimeout(TIMEOUT_WRITE_MS.toLong(), TimeUnit.MILLISECONDS)
            .readTimeout(TIMEOUT_READ_MS.toLong(), TimeUnit.MILLISECONDS)
            .build()

    private fun OkHttpClient.performGetRequest(url: String): Response =
        newCall(
            Request.Builder()
                .url(url)
                .get()
                .build()
        ).execute()
}

And I have been using Mapbox.Geocoder since my project is based on it. Here is my implementation of the Geocoder:

class GeocoderImpl @Inject constructor(
    private val runtime: RuntimeMapInfo,
) : Geocoder {
    override fun getCoordinatesForAddress(address: String): Coordinates? =
        MapboxGeocoding.builder()
            .accessToken(runtime.mapboxAccessToken())
            .query(address)
            .build()
            .executeCall()
            .body()
            ?.features()
            ?.preferPoiOrAddress()
            ?.geometry()
            ?.let { (it as? Point) }
            ?.toCoordinates()

    /**
     * Takes first "placeType=[poi]" or "placeType=[address]" otherwise any first feature.
     */
    private fun List<CarmenFeature>.preferPoiOrAddress(): CarmenFeature? =
        firstOrNull { feature ->
            feature.placeType()?.let {
                it.contains("address") || it.contains("poi")
            } == true
        } ?: firstOrNull()
}

Disclaim: I understad This is not the best solution, but it works. I have spent a day trying to figure out it. Maybe it will help someone to save a day :-)

PS:

Another, more hacky way could be ->
Ofc Ggoole.Maps knows the best how to unfold it's own shortened Urls. So if to rise a transparent webview and invoke there the shorted url then the one of redirects will be like "Case 3".

Update:

Also good to know that:

<intent-filter>
                <action android:name="android.intent.action.SEND"></action>
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>

Adds your app not only to Google.Maps but to Youtube, Chrome and bunch of other applications as well. Which is not great.

Alexander Skvortsov
  • 2,696
  • 2
  • 17
  • 31