We Wish You a Merry Christmas

Total notes: 54
Unique notes: 8

Unique notes:

  • C5
  • D5
  • E5
  • F5
  • G5
  • A5
  • B5
  • C6

Notes:

C5 F5 F5 G5 F5 E5 D5 D5 D5
G5 G5 A5 G5 F5 E5 E5 E5
A5 A5 B5 A5 G5 F5 D5 C5 C5 D5 G5 E5 F5
C5 F5 F5 F5 E5
E5 F5 E5 D5 C5
G5 A5 G5 G5 F5 F5 C6 C5
C5 C5 D5 G5 E5 F5

Durations:

1, 1, 0.5, 0.5, 0.5, 0.5, 1, 1, 1,
1, 0.5, 0.5, 0.5, 0.5, 1, 1, 1,
1, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.5, 1, 1, 1, 2,
1, 1, 1, 1, 1,
1, 1, 1, 1, 1,
1, 1, 0.5, 0.5, 0.5, 0.5, 1, 1,
0.5, 0.5, 1, 1, 1, 1

Jingle bells

Total notes: 90
Unique notes: 9
Credits: unknown

Unique notes:

  • E5
  • F#5
  • G#5
  • A5
  • B5
  • C#6
  • D6
  • E6
  • F#6

Notes:

E5 C#6 B5 A5 E5
E5 C#6 B5 A5 F#5
F#5 D6 C#6 B5 G#5
E6 F#6 E6 D6 B5 C#6
E5 C#6 B5 A5 E5
E5 C#6 B5 A5 F#5
F#5 D6 C#6 B5 E6 E6 E6
F#6 E6 D6 B5 A5
C#6 C#6 C#6
C#6 C#6 C#6
C#6 E6 A5 B5 C#6
D6 D6 D6 D6
D6 C#6 C#6
C#6 B5 B5 C#6 B5 E6
C#6 C#6 C#6
C#6 C#6 C#6
C#6 E6 A5 B5 C#6
D6 D6 D6
D6 C#6 C#6 C#6
E6 E6 D6 B5 A5

Durations:

1, 1, 1, 1, 4,
1, 1, 1, 1, 4,
1, 1, 1, 1, 3,
1, 1, 1, 1, 1, 4,
1, 1, 1, 1, 4,
1, 1, 1, 1, 4,
1, 1, 1, 1, 1, 1, 2,
1, 1, 1, 1, 4,
1, 1, 2,
1, 1, 2,
1, 1, 1.5, 0.5, 4,
1, 1, 1.5, 0.5,
1, 1, 2,
1, 1, 1, 1, 2, 2,
1, 1, 2,
1, 1, 2,
1, 1, 1.5, 0.5, 4,
1, 1, 2,
1, 1, 1.5, 0.5,
1, 1, 1, 1, 4

YouTube Shared

Android app that converts YouTube video link into ads-free browser video link.

🎧 Love listening to audiobooks on YouTube, but dealing with ads and the inability to play videos in the background without a premium subscription can be a challenge. 🤔

I noticed that these two drawbacks are absent in embedded YouTube videos found on external websites. By obtaining the video ID and using it in an embedded link template, you can turn any video link into an ad-free, background-playable version. This link can be opened directly in your browser, allowing for uninterrupted playback.

Initially, I manually performed these steps since I lacked Android app development skills. However, after learning a few things, I decided to automate the process of redirecting videos from the YouTube app to the browser. 🔄

Disclaimer

This application is created with no malicious intent. It is not intended to harm or interfere with any actions of Google or advertisers on the YouTube platform. The application solely automates specific actions that a regular user could manually perform. The functionality of the application adheres to all laws and does not alter any standard functionalities of the Android platform or other applications. It merely utilizes the capabilities provided by the Android platform and other applications.

Functionality requirements

Let's first determine the functions our application should have:

  • No GUI - this app is just a 'proxy.'
  • It should be able to receive shared text, including links from the YouTube app.
  • Actions with the link:
    1. Extract ID.
    2. Create an 'embedded link' with the received ID.
    3. Open the 'embedded link' in the browser (avoid launching the YouTube app as the default app for YouTube links).

Main Activity implementation

This is what an embedded link looks like:

https://www.youtube.com/embed/video-ID

YouTube may share the link of the video in two formats:

  • https://youtu.be/video-ID
  • https://youtube.com/watch?v=video-ID&other=parameters

To extract the video ID, we can use RegExp:

https?://youtu(\.be/|be\.com/watch\?v=)(?<videoId>[A-Za-z0-9_\-]+)

where "videoId" is a group name for symbols which should be extracted.

Then, concatenate https://www.youtube.com/embed/ with the extracted video ID.

To create an intent to launch the browser instead of the YouTube app by default:

final Intent intent = Intent.makeMainSelectorActivity( Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER) .setData(uri);

Configuration of the Manifest.xml

To prevent our app from having duplicate instances:

<application
  android:launchMode="singleTask"

Ability to receive shared text:

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

Results

Let's check the log to be sure that our app works fine:

11:49:06.283 onCreate 11:49:06.283 receivedText = https://youtube.com/watch?v=video-ID&si=a_bCDEfj1-HIjK-m 11:49:06.283 videoId = video-ID 11:49:06.284 uri = https://www.youtube.com/embed/video-ID 11:49:06.313 processReceivedText(): finished 11:49:07.134 onDestroy()

Here, you can see the next sequence:

  1. The app receives shared text with the link to the YouTube video.
  2. The video ID is successfully extracted.
  3. An embedded link for the browser is generated.
  4. After sending the converted link to the browser, our app quickly finishes its execution.

When I finished implementing the 'share with the browser' functionality, I decided to expand the capabilities of my app a bit. There is another drawback on YouTube that my application can address – the absence of the "Open as Regular Video" button for "Shorts" in order to access the playback speed control feature.

There is a "Shorts" link format:

https://youtube.com/shorts/video-ID

So, we need just convert it to:

https://youtube.com/watch?v=video-ID

or:

vnd.youtube:video-ID

and open in a YouTube app:

final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);

And since our application has completed its tasks, it is necessary to notify Android that it can be unloaded from memory by calling the finish() method.

Special Cases

When I tested my app on my old smartphone with MIUI Android, I encountered an issue where the intended app wouldn't start. I discovered that it could not run when my app was shut down by calling finish(), but it started as designed without this call. I received the error message in the logs:

com.android.server.am.ExtraActivityManagerService
MIUILOG-
Permission Denied Activity :
Intent {
  sel=act=android.intent.action.MAIN
  cat=[android.intent.category.APP_BROWSER]}
....

After spending some time searching on the Internet, I found out that the problem is related to a specific MIUI security permission - Start in background, and the user should manually enable it for our app:

Another case

If the author of the video has disabled embedding, it won't be possible to watch such a video using the method described in this article. However, it's worth noting that such cases are extremely rare.

Sources

GitHub: https://github.com/asilichenko/android-youtube-shared

Releases APK

Документ не прийнято. Невірний підпис - сертифікат відсутній в реєстрі зареєстрованих сертифікатів

  1. Деактивувати Дія.Підпис
  2. Відкликати попередній сертифікат там де створювали/замовляли
  3. Активувати Дія.Підпис
  4. Почекати добу
  5. Надіслати "Запит про отримання витягу щодо розрахунків з бюджетами та цільовими фондами за даними органів ДПС" (F1300204):
    1. Кабінет платника
    2. Введення звітності
    3. Рік: поточний
    4. Місяць: поточний
    5. Створити
    6. F13 Запити
    7. Потрібний документ буде першим у списку
    8. Створити, Перевірити, Підписати, Відправити
  6. Коли надійде друга квитанція за запитом - можна буде надсилати декларацію.

Android edge-to-edge layout

Default layout

Base theme without action bar:

<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
</style>

Default layout

Set background color for status and navigation bars

<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
  <item name="android:statusBarColor">@android:color/holo_purple</item>
  <item name="android:navigationBarColor">@android:color/holo_red_dark</item>
</style>

Colored status and navigation bars

Make status and navigation bars transparent

<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
  <item name="android:statusBarColor">@android:color/transparent</item>
  <item name="android:navigationBarColor">@android:color/transparent</item>
</style>

Transparent status and navigation bars

Make status bar text color contrasted

<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
  <item name="android:windowLightStatusBar">true</item>
  ...
</style>

windowLightStatusBar = true -> status bar will be drawn compatible with a light background

Status bar text color compatible with a light background

Make the layout fit the screen edge-to-edge

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
  ...
  WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
}

Layout fits the screen edge-to-edge

Layout is letterboxed in landscape mode

Layout is cutout in landscape mode

This 'bug' is present even in Google products like Maps and Google Earth; Sky Map is even displayed in full cutout mode.

Make layout fit the screen edge-to-edge in landscape mode

<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
  ...
  <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>

windowLayoutInDisplayCutoutMode can take one of three values:

  • default: content renders into the cutout area when displayed in portrait mode, but is letterboxed when in landscape mode
  • shortEdges: content always renders into the cutout area
  • never: content never renders into the cutout area

Layout edge-to-edge in landscape mode

windowLayoutInDisplayCutoutMode requires API level 27, so if your app supports lower API level, then extract it into "values-v27/themes.xml".

  • values/themes.xml:
<style name="Base.Theme.ShortEdges" parent="Theme.Material3.Light.NoActionBar">
  ...
</style>

<style name="Theme.ShortEdges" parent="Base.Theme.ShortEdges" />
  • values-v27/themes.xml:
<style name="Theme.ShortEdges" parent="Base.Theme.ShortEdges">
  <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>

How to determine safe region

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
  ...
  ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.my_view), this::onApplyWindowInsets);
}

@NonNull
public WindowInsetsCompat onApplyWindowInsets(@NonNull View v, @NonNull WindowInsetsCompat windowInsets) {
  final Insets displayCutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
  final Insets systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
  
  final Insets safeInsets = Insets.of(
    max(displayCutoutInsets.left, systemBarsInsets.left),
    max(displayCutoutInsets.top, systemBarsInsets.top),
    max(displayCutoutInsets.right, systemBarsInsets.right),
    max(displayCutoutInsets.bottom, systemBarsInsets.bottom)
  );

  return WindowInsetsCompat.CONSUMED;
}

Set component view margins

final ViewGroup.MarginLayoutParams mlp = 
  (ViewGroup.MarginLayoutParams) view.getLayoutParams();

mlp.leftMargin = safeInsets.left;
mlp.topMargin = safeInsets.top;
mlp.bottomMargin = safeInsets.bottom;
mlp.rightMargin = safeInsets.right;

view.setLayoutParams(mlp);

The button is in the safe zone

Resources

How to receive shared text in the Android App

First of all, we need to register our app as the plain text receiver by adding the intent filter into the Manifest for a certain activity:

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

The standard way to receive shared text is by calling the getStringExtra method for the intent:

final Intent intent = getIntent();
final String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);

It is also useful to preliminarily check the intent action:

final Intent intent = getIntent();
if (Intent.ACTION_SEND.equals(action)) {
  ...
}

This code can be called either from the onCreate method if we expect to receive data when the app is started, or from the onResume method if the app has already started and is in the background.

However, there is an issue when the app is called from the background - sometimes we receive the android.intent.action.MAIN action instead of SEND, and the extra is empty. But why does this happen? When does "sometimes" occur? Let's explore this issue.

Test case #1

  1. Our app is shut down
  2. Open Web-browser
  3. Share some link to the app
  4. The app is opened
  5. Method onCreate is called
  6. Shared text successfully received
  7. Switch back to the Web-browser
  8. Share link again
  9. The onResume method is called this time, and the data can be received.

Android activity lifecycle: Test case #1
 Nothing odd so far. Let's take a look Test case #2:

Test case #2

  1. Our app is shut down
  2. Start the app
  3. Hide the app into background
  4. Open the Web-browser
  5. Share link to the app
  6. The onResume method is called, but this time we got no data

Android activity lifecycle: Test case #2

Let's look into the logs, there we can see a pair of interesting records:

ActivityTaskManager I START 
  u0 {act = android.intent.action.SEND
    typ = text/plain
    flg = 0x13080001 
    cmp = ua.in.asilichenko.sharedtextreceiver/.MainActivity 
    clip = {text/plain {T(475)}}
    (has extras)
  } from uid 10191

ActivityTaskManager I Launching r: 
  ActivityRecord {
    90f60f2
    u0
    ua.in.asilichenko.sharedtextreceiver/.MainActivity
  }
  from background: 
    ActivityRecord {
      90e8155
      u0
      com.android.chrome/com.google.android.apps.chrome.Main
    } 
    t24314}. 
    New task: false

The conclusions that can be drawn from this log are:

  • Action android.intent.action.SEND with some data was sent indeed, but the app did not receive it
  • There is some flag: 0x13080001
  • The ActivityTaskManager determined that the app had already run, so it didn't start a new instance but restored the one from the background

The solution to this problem lies in overriding the onNewIntent method:

This is called for activities that set launchMode to "singleTop" in their package, or if a client used the Intent.FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity. In either case, when the activity is re-launched while at the top of the activity stack instead of a new instance of the activity being started, onNewIntent() will be called on the existing instance with the Intent that was used to re-launch it.
An activity can never receive a new intent in the resumed state. You can count on onResume being called after this method, though not necessarily immediately after the completion this callback. If the activity was resumed, it will be paused and new intent will be delivered, followed by onResume. If the activity wasn't in the resumed state, then new intent can be delivered immediately, with onResume() called sometime later when activity becomes active again.
Note that getIntent still returns the original Intent. You can use setIntent to update it to this new Intent. Dispatches this call to all listeners added via addOnNewIntentListener(Consumer). Handle onNewIntent() to inform the fragment manager that the state is not saved. If you are handling new intents and may be making changes to the fragment state, you want to be sure to call through to the super-class here first. Otherwise, if your state is saved but the activity is not stopped, you could get an onNewIntent() call which happens before onResume() and trying to perform fragment operations at that point will throw IllegalStateException because the fragment manager thinks the state is still saved.

So it turns out that if the sending application launches our application, the data will continue to be successfully received through the intent. However, in the case where the application was launched separately from the sender, a new intent will be created each time, and the sent data will not be transferred to the new intent. Nevertheless, the original intent from the sender can be obtained in the onNewIntent method.

@Override
protected void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  setIntent(intent);
  receiveText();
}
Android activity lifecycle: Test case #2. Fix

Now, both test cases works fine, but I found another bug.

Test case #3

  1. Our app is in the background
  2. Open Web-browser
  3. Share a link
  4. The app is opened
  5. Method onCreate is called

Android activity lifecycle: Test case #3

Now, if we look in the background stack, we can see two instances of our app, and that's not good.

To fix this, we need to set launchMode for the main activity to "singleTask":

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:launchMode="singleTask">

In this mode, if there is already an existing instance of the activity in the system, a new one will not be created, and the existing one will be called. 

Android activity lifecycle: Test case #3. Fix

Let's check one more case.

Test case #4

  1. Share data into our app
  2. Data is received
  3. Rotate the device
  4. The same data is received again

This happens because when the Android device is rotated, it recreates the activity, but the intent remains the same. As a result, the processing of shared text in the onCreate method is triggered again, and the read data is also re-read.

Android activity lifecycle: Test case #4

To avoid re-reading the data, you need to remove them from the intent using the removeExtra method:

if (Intent.ACTION_SEND.equals(action)) {
  final String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
  if (null != stringExtra) {
    ...
    intent.removeExtra(Intent.EXTRA_TEXT);
  }
}

Android activity lifecycle: Test case #4. Fix

That's all for today.

Unlocking Google Street View: Extracting Panorama ID and Location from the Google Maps Link

Ever wondered how Google works its magic with Maps? 🗺️ They've got these cool Maps URLs that hold location secrets, like where to find panoramas and other goodies. 🌍🌆

I got curious during my little pet project and needed some of that location jazz and panorama ID pizzazz. But guess what? Google keeps the recipe for those URLs hush-hush. 🤫 So, I put on my virtual Sherlock Holmes hat 🕵️ and went on an adventure to reverse-engineer the map-magic. 🚀 Now, I'm here to share the treasure map with you! X marks the spot! 📍🗝️ 

Use cases

User shares a location or panorama from the Google Maps app or web-site to our app. As a result, a shortened link is received. Due to Google's redirection from the shortened URL to the full URL, we should send an HTTP request to the shortened link to receive the full URL. This URL will be parsed to gain useful information.

There are several cases of shared locations:

  1. Point
  2. Place
  3. Panorama
  4. User panorama
  5. Search for a location, place or address
  6. Special case #1: Place + Map moved
  7. Special case #2: Place + Panorama

Other sections:

  1. Web-API: Panorama preview
  2. Web-API: Panorama metadata
  3. Web-API: Place information
  4. Web-API: Sign your request
  5. Android Place API
  6. Android Geocoder
  7. CID issue
  8. Conclusion

1. Point

Web-browser shared link:

goo.gl/maps/Up94VLPN5kPJ74WEA

Unshortened link:

www.google.com/maps/search/48.718116,+2.627758?entry=tts&shorturl=1

I use domain ".com" in all my examples, but globally it can be any of supported ones: supported_domains.

Query segment may be discarded:

www.google.com/maps/search/48.718116,+2.627758

Plus sign is an URL encoded space sign.

So, we can get the location of the place: (lat: 48.718116, lon: 2.627758). Nevertheless, I assume that there are cases when value of this attribute contains not coordinates, but rather an address or a description of the place.

The link from the mobile app has a different domain:

maps.app.goo.gl/7qWFiAfvRsBEKTz56

is redirected to:

www.google.com/maps/place/ -13.165331,-72.548991/ data = !4m6 !3m5 !1s0 !7e2 !8m2 !3d -13.1653311 !4d -72.5489906

Сoordinates are also provided as path attributes, and additionally, they are present in the data path attribute.

The "data" segment consists of sections, each starting with an exclamation mark "!", followed by a digit, a letter, and the data. Let's describe some of them:

  • !4m6 - "m" stands for a container, first "4" stands for "container of containers", first "3" - "container of values", last "6" is the number of included items
  • !8m2 - coordinates container, two children included
  • !3d - latitude, double value
  • !4d - longitude, double value

A point can be represented as a place with coordinates in DMS format, serving as a name:

      
www.google.com/maps/place/ 48°43'05.4"N+2°37'40.4"E/ @48.7181667,2.6278889,17z/ data = !3m1 !4b1 !4m4 !3m3 !8m2 !3d 48.7181667 !4d 2.6278889
  • 17z - stands for "zoom level" = 17

2. Place

Web-link:

www.google.com/maps/place/ Buckingham+Palace/ @51.5016211,-0.1428878,17z/ data = !3m1 !5s 0x487605276d38fb6b:0xe1c60228d7946675 !4m6 !3m5 !1s 0x48760520cd5b5eb5:0xa26abf514d902a7 !8m2 !3d 51.5016211 !4d -0.1428878 !16z L20vMDE5OGc !5m1 !1e4

App-link:

www.google.com/maps/place/ Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/ data = !4m2 !3m1 !1s 0x48760520cd5b5eb5:0xa26abf514d902a7

The useful information is limited just to the place name.

3. Panorama

Web-link:

www.google.com/maps/ @48.8583445,2.2943296,2a,75y,313.07h,81.5t/ data = !3m7 !1e1 !3m5 !1s _BgQhZS8mY-Zix5BdCt2mA !2e0 !6s https://streetviewpixels-pa.googleapis.com/v1/thumbnail ? panoid = _BgQhZS8mY-Zix5BdCt2mA & cb_client = maps_sv.tactile.gps & w = 203 & h = 100 & yaw = 258.24527 & pitch = 0 & thumbfov = 100 !7i 13312 !8i 6656 !5m1 !1e4
  • 1e1 - is an indicator that data contains panorama info.

The panorama ID is always located in the "!1s" field and followed either with "!2e0" or "!2e10", where "0" indicates that the panorama belongs to Google and "10" - to the private person (user). Moreover, for user panoramas, this string won't exactly be the panorama ID, but we'll get to that later.

So, the panorama ID can be preceded by such a set of characters:

  • !3m7!1e1!3m5!1s
  • !3m6!1e1!3m4!1s
  • !3m4!1e1!3m2!1s

@-path attribute contains camera settings:

  • 48.8583445,2.2943296 - coordinates (latitude, longitude)
  • 75y - vertical view angle, limited: [15, 90]
  • 313.07h - heading / bearing / azimuth
  • 81.5t - tilt / pitch

IMPORTANT: all slashes in data section must be encoded as "%2F".

App-link:

www.google.com/maps/ @48.858345,2.2943299,0a,75y,312.57h,84.6t/ data = !3m4 !1e1 !3m2 !1s _BgQhZS8mY-Zix5BdCt2mA !2e0

4. User panorama

Web-link:

www.google.com/maps/ @50.45007,30.5238129,3a,48.9y,228.12h,97.74t/ data = !3m6 !1e1 !3m4 !1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5 !2e10 !7i 12082 !8i 6041

So, here we can see the "!2e10" ending after the panorama ID section, and it indicates that this is a user panorama. The issue is that the panorama ID in such URLs is not the ID of the actual panorama; it's more like a "creator panorama ID" which can't be used to obtain this panorama by API. I haven't been able to figure out how to translate it into the actual panorama ID.

The only option we have is to use the shooting location's coordinates and hope that Google consistently finds the desired panorama. My expectation is based on the fact that user-generated panoramas are individual and occupy specific spots with some accuracy on the map. Furthermore, these custom panoramas lack the "history stack" feature ("history stack" allows you to select a panorama based on its capture date for a particular location). Nevertheless, if I come across a relevant case, then I will find an alternative way to obtain the correct panorama.

App-link almost the same:

www.google.com/maps/ @50.4500732,30.5238113,0a,75y,231.11h,93.48t/ data = !3m4 !1e1 !3m2 !1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5 !2e10

But there is one nuance: not all user panoramas that open in the browser can be accessed through the API. This situation arises because user panoramas are associated with the Google Photos service, and in order for a panorama to be opened through the Street View Panorama API, it must have an identifier in the Google Street View service. Unfortunately, this is not always the case. To check if a panorama is registered in the Google Street View service, you can look for a blue dot at the location of the panorama. If it's not there, then it will be impossible to find this panorama through the Google Street View API.

For example, there is a registered panorama, blue dot is under the Pegman: 

Chernihiv. Hotel "Ukraine" after being hit by a ruZZian missile.

  • photo ID: AF1QipN9GteezKnVzb23gMzWsirXHLDqeTreSvuQzIsc
  • panorama ID: CAoSLEFGMVFpcE45R3RlZXpLblZ6YjIzZ016V3NpclhITERxZVRyZVN2dVF6SXNj
  • location: 51.494505427553,31.294799751616

There is a not registered panorama, no blue dot under the Pegman:

  • photo ID: AF1QipOi38JA6bFk_E9ERR7kNDPkMt6VMIxvjQHkpGSv
  • location: 51.5250583,31.3265528

5. Search for a location, place or address

These links are generated in the mobile app only, but not for all search results; usually, they are place-links.

  • Location
www.google.com/maps?q=50.813588,-2.475488
  • Place
www.google.com/maps?q=London,+Great+Britain
  • Address
www.google.com/maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA

6. Special case #1: Place + Map moved

  1. Click on any place-marker.
  2. Move map camera away.
  3. Click on Share button on the Marker info window.

Example:

  • Place: Empire State Building
  • Map camera: East River
www.google.com/maps/place/ Empire+State+Building/ @40.7321378,-73.9691401,17.87z/ data = !3m1 !5s 0x8b398fecd1aea119:0x76fa1e3ac5a94c70 !4m6 !3m5 !1s 0x89c259a9b3117469:0xd134e199a405a163 !8m2 !3d 40.7484405 !4d -73.9856644 !16z L20vMDJuZF8
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7321378,-73.9691401 = 40°43'55.7"N, 73°58'08.9"W - East River
  • data-location:
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building

Result:

  • place = marker place name
  • @location = map camera location
  • data-location = place marker location

7. Special case #2: Place + Panorama

  1. Select any place marker.
  2. Turn on Street View layer.
  3. Move map camera away.
  4. Open panorama on any place, not the original place.
  5. Click Share on panorama view (under triple dots menu on top right corner).

Example:

  • Place: Empire State Building
  • Panorama: Flatiron Building
www.google.com/maps/place/ Empire+State+Building/ @40.7413886,-73.9891371,3a,75y,230.15h,116.97t/ data = !3m6 !1e1 !3m4 !1s nhbQjZVsxbtfxb9rTUhZmQ !2e0 !7i 16384 !8i 8192 !4m6 !3m5 !1s 0x89c259a9b3117469:0xd134e199a405a163 !8m2 !3d 40.7484405 !4d -73.9856644 !16z L20vMDJuZF8
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7413886,-73.9891371 = 40°44'29.0"N, 73°59'20.9"W - Flatiron Building
  • data-location:
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building

Result:

  • place = place name of the marker
  • @location = panorama location
  • data-location = place marker location

8. Web-API: Panorama preview

  • by location:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & location = 48.8589363,2.295527 & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY
  • by ID:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & pano = CAoSLEFGMVFpcE81WUZKcGNmeVFjX1FBa0dUUWJ6VVh3eUdZcDFXRnB6SF9mVmFw & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY

9. Web-API: Panorama metadata

  • by location:
maps.googleapis.com/maps/api/streetview/metadata ? location = 40.7143888,-74.0016005 & key=API_KEY
  • by ID:
maps.googleapis.com/maps/api/streetview/metadata ? pano = CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5 & key=API_KEY

Result:

{ "copyright" : "© Matthew MacVey", "date" : "2018-05", "location" : { "lat" : 40.7143888, "lng" : -74.0016005 }, "pano_id" : "CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5", "status" : "OK" }

10. Web-API: Place information

maps.googleapis.com/maps/api/place/textsearch/json ? query = Big-Ben & key = API_KEY

Result:

{     "html_attributions": [],     "results": [         {             "business_status": "OPERATIONAL",             "formatted_address": "London SW1A 0AA, United Kingdom",             "geometry": {                 "location": {                     "lat": 51.50072919999999,                     "lng": -0.1246254                 },                 "viewport": {                     "northeast": {                         "lat": 51.50211262989271,                         "lng": -0.12381395                     },                     "southwest": {                         "lat": 51.49941297010727,                         "lng": -0.1269553499999999                     }                 }             },             "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png",             "icon_background_color": "#7B9EB0",             "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet",             "name": "Big Ben",             "photos": [                 {                     "height": 3970,                     "html_attributions": [                         "<a href=\"https://maps.google.com/maps/contrib/108563206122115577170\">Miguel Ángel Romacho Contreras</a>"                     ],                     "photo_reference": "AUacShixy_iNsH-O1SgdK6sCUEpqgarAI8y-xpQ942CNSVrqddsB6cIyQ19VE6v6UijDZRptsWhZqA3IgQSyZ2ewIq2nxdtVhy2ywoE5FD7T6w_tP5_4T18UZKCZIR84F0bemWL_TZiSKyDUbzPnke_ciTSgH8EsyMV_8BLz6x2IpQ0iGoBO",                     "width": 5955                 }             ],             "place_id": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",             "plus_code": {                 "compound_code": "GV2G+74 London, United Kingdom",                 "global_code": "9C3XGV2G+74"             },             "rating": 4.4,             "reference": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",             "types": [                 "tourist_attraction",                 "point_of_interest",                 "establishment"             ],             "user_ratings_total": 54585         }     ],     "status": "OK" }

11. Web-API: Sign your request

12. Android Place API

Places.initialize(appContext, mapsApiKey); String placeId = "ChIJT9j2eKVv5kcRp9MrU92slfY"; final PlacesClient placesClient = Places.createClient(activityContext); final List<Place.Field> fields = singletonList(Place.Field.LAT_LNG); final FetchPlaceRequest request = FetchPlaceRequest.newInstance(placeId, fields); placesClient.fetchPlace(request)   .addOnSuccessListener(response -> {     final Place place = response.getPlace();     final LatLng location = place.getLatLng());   }).addOnFailureListener(exception -> {     logger.error("Place not found:", exception);   });

13. Android Geocoder

final Geocoder geocoder = new Geocoder(appContext); String searchString = "Big Ben"; final List<Address> addresses = geocoder.getFromLocationName(searchString, 1); LatLng location = null; if (null != addresses && !addresses.isEmpty()) {   final Address address = addresses.get(0);   location = new LatLng(address.getLatitude(), address.getLongitude()); }

14. CID Issue

Sometimes, Google Maps app generate unusual link with parameter "cid":

maps.google.com/?cid=13226830714359798441

CID (Customer ID Number) is a unique Google identifier of a business entity. In other words, Google uses this number to identify an account of a business and cluster business information across GMB, Ads, Analytics, Maps and other Google’s properties.

Using a webservice API, you can retrieve information about a place using the specified identifier:

maps.googleapis.com/maps/api/place/details/json   ? cid = CID   & key = API_KEY

But this method is unsafe for use in an Android application because all the keys are stored within the app, and theoretically, an attacker could extract them from there. However, using a Maps API key can be secured at the Google Console level by specifying 'Set an application restriction' and 'Android Fingerprint restriction.' Unfortunately, such restrictions cannot be applied to a key used for retrieving information through a webservice, creating a theoretical vulnerability for third-party use of this key.

However, there is another way to obtain the necessary information. It involves making a request to the provided link, parsing the HTML page of the response body, and extracting the metadata containing the place's name. From there, it's straightforward – using a geocoder, we can find the location.

Meta-information in the response html:

<meta content="Big Ben · London SW1A 0AA, United Kingdom" itemprop="name"> <meta content="Big Ben · London SW1A 0AA, United Kingdom" property="og:title"> <meta content="Big Ben · London SW1A 0AA, United Kingdom" property="og:site_name">

Send request to CID-URL, receive response and extract meta from the body: https://gist.github.com/asilichenko/b0000eb1562c9e4e75b0d43d799260bc

import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.io.IOException; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class MetadataExtractor { private static final String META_ITEMPROP_NAME = "meta[itemprop=name]"; private static final String META_PROPERTY_OG_TITLE = "meta[property=og:title]"; private static final String META_PROPERTY_OG_SITE_NAME = "meta[property=og:site_name]"; private static final String CONTENT = "content"; private final OkHttpClient httpClient; public MetadataExtractor() { this.httpClient = new OkHttpClient(); } public Metadata extract(String url) { final Request request = new Request.Builder().url(url).build(); try (final Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Request failed: " + response.code()); } Metadata retval = null; final ResponseBody body = response.body(); if (null != body) { final String html = body.string(); final Document doc = Jsoup.parse(html); final String name = getMetaContent(doc, META_ITEMPROP_NAME); final String title = getMetaContent(doc, META_PROPERTY_OG_TITLE); final String siteName = getMetaContent(doc, META_PROPERTY_OG_SITE_NAME); retval = new Metadata(name, title, siteName); } return retval; } catch (IOException ioe) { throw new RuntimeException(ioe); } } /** * Get an attribute value from the first matched element * that has the attribute. */ private String getMetaContent(Document doc, String selector) { return doc.select(selector).attr(CONTENT); } public static class Metadata { public final String name; public final String title; public final String siteName; public Metadata(String name, String title, String siteName) { this.name = name; this.title = title; this.siteName = siteName; } } }

15. Conclusion

In this article, I have presented various scenarios illustrating when a shared link on Google Maps can be generated.

Here are the conclusions I've drawn about the data in the links:

  • If a place or a marker is selected, then locations in the path and in the data both point to the selected place/marker.
  • Place name can be represented either as string or coordinates as pair of doubles or in DMS format.
  • If panorama is opened then panorama ID is present.
  • If a panorama is opened, then the @location equals the panorama's location; otherwise, the @location equals the map camera's location.
  • If the opened panorama belongs to the user, then the ID is useless, but the @location points to the panorama's location.
  • Mobile Google Maps currently does not support having both a selected marker and an opened panorama simultaneously. Therefore, all locations in the link are always consistent and point to user's desired location.

Now, let's explore the various types of Google Maps links I've identified. I've simplified them by removing unnecessary details to focus on the fundamental structure.

  • point -> web:
/maps/search/48.718116,+2.627758
  • point -> app:
/maps/place/ -13.165331,-72.548991/ data = !8m2 !3d -13.1653311 !4d -72.5489906
  • place -> web:
/maps/place/ Empire+State+Building/ @40.7321378,-73.9691401,17.87z/ data = !8m2 !3d 40.7484405 !4d -73.9856644
  • place + panorama:
/maps/place/ Empire+State+Building/ @40.7413886,-73.9891371,3a,75y,230.15h,116.97t/ data = !1s nhbQjZVsxbtfxb9rTUhZmQ !2e0 !8m2 !3d 40.7484405 !4d -73.9856644
  • place -> app:
/maps/place/Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/
  • panorama:
/maps/ @48.858345,2.2943299,0a,75y,312.57h,84.6t/ data = !1s _BgQhZS8mY-Zix5BdCt2mA !2e0
  • query:
/maps?q=50.8136769,-2.4746919 /maps?q=London,+Great+Britain /maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA

Finally, let's define the general scheme of link structure based on coordinates and panorama ID:

/maps/search/COORDINATES /maps/search/STRING === /maps/place/NAME /maps/place/NAME/ @COORDINATES_MAP/ data=COORDINATES_PLACE /maps/place/NAME/ @COORDINATES_PANORAMA/ data=PANORAMA_ID !COORDINATES_PLACE /maps/place/ COORDINATES/ data=COORDINATES === /maps/@COORDINATES /maps/@COORDINATES/data=PANORAMA_ID === /maps?q=COORDINATES /maps?q=STRING === maps.google.com/?cid=CID /maps?cid=CID

Thanks for your time and best of luck with your mapping and panoramic adventures!

Desperate Housewives Susan's Art s05 e16

Desperate Housewives Susan's Art s05 e16