Русский
Русский
English
Статистика
Реклама

Google maps

Как стать владельцем чужой организации в Google Maps?

03.12.2020 00:05:42 | Автор: admin

Одним тёплым вечером жена сказал что стала владельцем нашим Музеем Мирового океана, находящимся в Калининграде. Она просто нажала на кнопку "Я владелец компании" в Google картах.

Я не поверил этому, как такое вообще может быть? Для подтверждения она изменила адрес сайта в профиле организации, через минуту в нём стал отображаться новый URL. Передо мной стоял и улыбался новый владелец крупного музея.

Мы сразу же вернули настоящий URL и принялись захватывать другие организации. Разумеется ничего не вышло. Каждый раз Google требовал подтверждения права собственности с помощью кода подтверждения из электронного письма, звонка по телефону или почтового уведомления.

Но как тогда получилось завладеть музеем? В тот вечер все мысли сводились к найденной уязвимости в системе верификации прав собственности на организации. Сразу был отправлен репорт в Google Bug Bounty со всей имеющейся информацией. А после я начал искать причины такого поведения системы верификации чтобы дополнить репорт новой информацией.

За несколько дней я смог захватить и получить полный контроль над 11 организациями в Google Business. Я мог изменять информацию в профиле, загружать картинки, отвечать на отзывы, просматривать статистику и.т.д.

Профили захваченных организаций на Google My Business

У меня не получалось захватывать конкретные организации, все захваты получались наугад, ручным перебором по карте. Я не смог определить закономерность. Какие-либо манипуляции с моей стороны не увеличивали шансы на захват конкретной организации. Единственное моё заключение - всё происходит со стороны Google. Я записывал видео, делал скриншоты и сохранял HAR, для дальнейшей отправки инженерам по безопасности Google.

Процесс захвата всегда оставался одним и тем же:

1) Выбрать организацию на Google Maps;

2) Нажать кнопку "Я владелец компании";

3) В Google Business нажать кнопку "Управлять компанией".

Видео процесса захвата

Google посчитал это не уязвимостью, ссылаясь на машинное бучение и модерацию информации в картах сотрудниками. Дополнительные доказательства также не привели к пересмотру репорта. Раз это не уязвимость, я решил внести изменения в профиль одной из захваченных организаций. Добавил номер репорта в блок комментария от компании. Разумеется изменения прошли модерацию, я всё таки имею полный контроль, и через несколько минут отобразились в профиле организации.

После чего я сообщил в Google о внесённых изменениях. По прошествии 2 недель ответ от него не поступил. Я решил внести ещё изменения, ответил на отзыв.

Прошло 2 недели, но ответа нет. Тогда я добавил номер репорта в блок комментария от компании во все захваченные организации. Разумеется и в этом случае Google молчал. Уже больше месяца внесённые изменения никто не удалил. Я решил внести кардинальные изменения в профиль одной из компаний.

Картиночка

В конечном счёте, через месяц я смог получить от Google ответ, с просьбой перестать им писать. За 2 месяца внесённые изменения никто не удалил. В качестве последней попытки доказать наличие уязвимости я внёс максимальное количество изменений во все профили захваченных организаций. Полностью нарушив деятельность организаций в Google Maps. Хорошо это или плохо? Скорее всего плохо, но другого способа привлечь внимание к наличию проблемы я не придумал. Так же нужно учитывать что с момента создания репорта прошло уже 3 месяца. Все правки прошли модерацию за час и карта стала выглядеть следующим образом:

Картиночка

Google отказался от пересмотра репорта сославшись на то, что получение полного контроля над профилем чужой организации - это не уязвимость. И наконец-то удалил все мои внесённые изменения. Неизвестно сколько бы они ещё продержались, не сообщи я о них. Месяц, год?

Вывод я сделал следующий - злоумышленник может захватить организацию и подменить адрес сайта на фишинговый, а жертва с радостью отправит ему свои деньги. Купит билеты в кино, оплатит новый iPhone, забронирует стол в ресторане, оплатит доставку еды и.т.д.

Подробнее..

JavaScript за 60 секунд работаем с картой (Geolocation API, Leaflet.js, Nominatim)

14.12.2020 12:16:00 | Автор: admin


Доброго времени суток, друзья!

В этом небольшом туториале мы вместе с вами выполним три простых задания:

  • С помощью Geolocation API и Leaflet.js определим текущее местоположение пользователя и отобразим его на карте
  • Реализуем анимированный переход между городами
  • Реализуем переключение между адресами с предварительным получением названия объекта и его координат

Код проекта находится здесь.

Поиграть с кодом можно здесь:


Определяем текущее местоположение пользователя


Geolocation API позволяет пользователю предоставлять веб-приложению данные о своем местоположении. В приложении для запроса этих данных используется метод Geolocation.getCurrentPosition(). Данный метод принимает один обязательный и два опциональных параметра: success функция обратного вызова, получающая объект Position при предоставлении разрешения, error функция обратного вызова, получающая объект PositionError при отказе в доступе, и options объект с настройками. Вот как это выглядит в коде:

navigator.geolocation.getCurrentPosition(success, error, {  // высокая точность  enableHighAccuracy: true})function success({ coords }) {  // получаем широту и долготу  const { latitude, longitude } = coords  const position = [latitude, longitude]  console.log(position) // [широта, долгота]}function error({ message }) {  console.log(message) // при отказе в доступе получаем PositionError: User denied Geolocation}

Отображаем местоположение пользователя на карте


В качестве карты мы будем использовать Leaflet.js. Данный сервис является альтернативой Google Maps и OpenStreetMap, уступает им по функционалу, но подкупает простотой интерфейса. Создаем разметку, в которой подключаем стили и скрипт карты:

<head>  <!-- стили карты -->  <link      rel="stylesheet"      href="http://personeltest.ru/aways/unpkg.com/leaflet@1.7.1/dist/leaflet.css"      integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="      crossorigin=""    />    <!-- скрипт карты -->    <script      src="http://personeltest.ru/aways/unpkg.com/leaflet@1.7.1/dist/leaflet.js"      integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="      crossorigin=""    ></script>    <!-- наши стили -->    <link rel="stylesheet" href="style.css" /></head><body>  <!-- контейнер для карты -->  <div id="map"></div>  <!-- кнопка для вызова функции -->  <button id="my_position">My Position</button>  <!-- наш скрипт-модуль -->  <script src="script.js" type="module"></script></body>

Добавляем минимальные стили (style.css):

* {  margin: 0;  padding: 0;  box-sizing: border-box;}body {  min-height: 100vh;  display: grid;  place-content: center;  place-items: center;  background-color: rgb(241, 241, 241);}#map {  width: 480px;  height: 320px;  border-radius: 4px;  box-shadow: 0 0 1px #222;}button {  padding: 0.25em 0.75em;  margin: 1em 0.5em;  cursor: pointer;  user-select: none;}

Создаем модуль map.js следующего содержания:

// создаем локальные переменные для карты и маркера// каждый модуль имеет собственное пространство именlet map = nulllet marker = null// функция принимает позицию - массив с широтой и долготой// и сообщение, отображаемое над маркером (tooltip)export function getMap(position, tooltip) {  // если карта не была инициализирована  if (map === null) {    // второй аргумент, принимаемый методом setView - это масштаб (zoom)    map = L.map('map').setView(position, 15)  } else return  // что-то типа рекламы  // без этого карта работать не будет  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {    attribution:      ' <a href="http://personeltest.ru/aways/www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'  }).addTo(map)  // добавляем маркер с сообщением  L.marker(position).addTo(map).bindPopup(tooltip).openPopup()}

Наконец, создаем script.js:

// импортируем функциюimport { getMap } from './map.js'// находим кнопку и добавляем к ней обработчикdocument.getElementById('my_position').onclick = () => {  navigator.geolocation.getCurrentPosition(success, error, {    enableHighAccuracy: true  })}function success({ coords }) {  const { latitude, longitude } = coords  const currentPosition = [latitude, longitude]  // вызываем функцию, передавая ей текущую позицию и сообщение  getMap(currentPosition, 'You are here')}function error({ message }) {  console.log(message)}

Открываем index.html в браузере, нажимаем на кнопку, предоставляем разрешение на получение данных о местоположении, видим нашу позицию на карте.



Отлично. Двигаемся дальше.

Анимированный переход между городами


Предположим, что у нас имеется объект с тремя городами (Москва, Санкт-Петербург, Екатеринбург) и их координатами (db/cities.json):

{  "Moscow": {    "lat": "55.7522200",    "lon": "37.6155600"  },  "Saint-Petersburg": {    "lat": "59.9386300",    "lon": "30.3141300"  },  "Ekaterinburg": {    "lat": "56.8519000",    "lon": "60.6122000"  }}

Нам необходимо реализовать плавное переключение между этими городами на карте.

Добавляем в разметку контейнер для городов:

<div id="cities"></div>

Переписываем script.js:

import { getMap } from './map.js'// получаем контейнер для городовconst $cities = document.getElementById('cities');(async () => {  // получаем объект с городами  const response = await fetch('./db/cities.json')  const cities = await response.json()  // перебираем города  for (const city in cities) {    // создаем кнопку    const $button = document.createElement('button')    // текстовое содержимое кнопки - название города    $button.textContent = city    // получаем широту и долготу    const { lat, lon } = cities[city]    // записываем название города, широту и долготу    // в соответствующие data-атрибуты    $button.dataset.city = city    $button.dataset.lat = lat    $button.dataset.lon = lon    // добавляем кнопку в контейнер    $cities.append($button)  }})()// обрабатываем нажатие кнопки$cities.addEventListener('click', ({ target }) => {  // нас интересует только нажатие кнопки  if (target.tagName !== 'BUTTON') return  // получаем название города, широту и долготу из data-атрибутов  const { city, lat, lon } = target.dataset  const position = [lat, lon]  // вызываем функцию, передавая ей позицию и название города  getMap(position, city)})

Также немного изменим map.js:

let map = nulllet marker = nullexport function getMap(position, tooltip) {  if (map === null) {    map = L.map('map').setView(position, 15)  } else {    // перемещение к следующей позиции    map.flyTo(position)  }  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {    attribution:      ' <a href="http://personeltest.ru/aways/www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'  }).addTo(map)  // удаление предыдущего маркера  if (marker) {    map.removeLayer(marker)  }  marker = new L.Marker(position).addTo(map).bindPopup(tooltip).openPopup()}

Открываем index.html. При нажатии первой кнопки сразу получаем позицию и название города. При нажатии второй и последующих кнопок плавно перемещаемся между городами.



Плавное переключением между адресами


Предположим, что у нас имеются три объекта с названиями и адресами (db/addresses.json):

{  "Театр драмы": "Октябрьская площадь, 2",  "Театр оперы и балета": "Проспект Ленина, 46А",  "Коляда-Театр": "Проспект Ленина, 97"}

Нам необходимо реализовать переключение между этими объектами на карте. Но как нам это сделать без координат? Никак. Следовательно, нам каким-то образом нужно эти координаты получить. Для этого воспользуемся сервисом Nominatim от OpenStreetMap. О том, как правильно сформировать строку запроса, смотрите здесь. Я продемонстрирую лишь один из возможных вариантов.

Итак, создаем в разметке контейнер для адресов:

<div id="addresses"></div>

Переписываем script.js:

// получаем контейнер для адресовconst $addresses = document.getElementById('addresses');(async () => {  // названия и адреса объектов  const response = await fetch('./db/addresses.json')  const addresses = await response.json()  // для каждого места  for (const place in addresses) {    // создаем кнопку    const $button = document.createElement('button')    $button.textContent = place    // получаем адрес    const address = addresses[place]    // формируем строку запроса    const query = address.replace(      /([А-ЯЁа-яё]+)\s([А-ЯЁа-яё]+),\s([0-9А-ЯЁа-яё]+)/,      '$3+$1+$2,+Екатеринбург'    )    // получаем, например, 2+Октябрьская+площадь,+Екатеринбург    // записываем данные в соответствующие data-атрибуты    $button.dataset.address = address    $button.dataset.query = query    $addresses.append($button)  }})()// обрабатываем нажатие кнопки$addresses.addEventListener('click', async ({ target }) => {  if (target.tagName !== 'BUTTON') return  // получаем данные из data-атрибутов  const { address, query } = target.dataset  // получаем ответ от сервиса  const response = await fetch(    `https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=1`  )  // format - формат данных, limit - количество объектов с данными  // парсим ответ, извлекая нужные сведения  const { display_name, lat, lon } = (await response.json())[0]  // редактриуем название объекта  const name = display_name.match(/[А-ЯЁа-яё\s(\-)]+/)[0]  const position = [lat, lon]  // формируем сообщение  const tooltip = `${name}<br>${address}`  // вызываем функцию  getMap(position, tooltip)})

Открываем index.html. При нажатии первой кнопки сразу получаем позицию и название театра. При нажатии второй и последующих кнопок плавно перемещаемся между театрами.



Круто. Все работает, как ожидается.

На этом позвольте откланяться. Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание и хорошего дня.
Подробнее..

Перевод Шесть причин, по которым Google Maps самое пугающее приложение на вашем смартфоне

16.11.2020 00:15:06 | Автор: admin

Google знает, где вы находитесь как и рекламодатели




Приложение Google Maps, знает всё. Не только всё о каждой улице, кафе, баре и магазине на этой улице но и о людях, их посещающих. Ежемесячно сервисом пользуется миллиард человек, и это приложение прочно укоренилось в жизнях людей. Оно направляет их по пути на работу, в гости к друзьям и родственникам, на приём к врачу и во время зарубежных путешествий.

То, что у приложения Google Maps есть возможность следить за каждым вашим шагом, не означает автоматически, что оно этими возможностями злоупотребляет. Но могло бы что является проблемой само по себе, особенно из-за того, что штаб-квартира Google находится в США, где частная жизнь охраняется хуже, чем в Европе, а разведка замечена в слежке за гражданскими лицами.

Да, Google Maps очень полезное приложение. Однако есть несколько причин, по которым следовало бы ещё раз проверить все настройки конфиденциальности. А потом спросить себя, какой частью своих личных данных вы готовы жертвовать ради удобства.

Google Maps нужна история ваших поисков



Автоматические настройки при создании новой учётной записи в Google

Настройки Web & App Activity в Google описывают, как компания собирает данные о пользователях, в частности, о его местонахождении, чтобы обеспечивать ему быстрее работающий сервис и персонализированный контент. А проще говоря, что абсолютно все места, что вы искали в картах будь то стрип-клуб, шаурмячная или место, где вы встречаетесь со своим поставщиком наркотиков, приезжающим туда на мопеде сохраняются, интегрируясь в поисковый алгоритм Google на 18 месяцев.

В Google знают, что вам это кажется неприятным. Поэтому компания использует т.н. скрытые закономерности [dark patterns] интерфейсы, подталкивающие нас к выбору, который мы вряд ли сделали бы сами. Это может быть выделение одного из вариантов выбора другим шрифтом или более яркими цветами.

Для этой статьи журнал VICE создал новую учётную запись в Google, чтобы посмотреть, насколько сложно пользователю будет избегать скрытых закономерностей. Нажав на кнопочку Создать учётную запись, мы получили всплывающее окно, где нам мелкими серыми буковками было предложено изменить настройки для включения функций персонализации, а кнопка подтверждения была гораздо крупнее и синего цвета. Нажимая кнопку, мы соглашались к сбору активности в вебе и приложениях, упомянутому выше. В качестве альтернативы там была менее заметная кнопка Больше вариантов, по которой открывалась новая страница с длинными и сложными объяснениями. И потом всё равно нужно было вручную отключить сбор активности в вебе и приложениях.

Мы отправили в пресс-офис Google 12 вопросов, и представитель компании ответила нам в том плане, что Google хочет, чтобы настройки было легко найти и просто использовать. Она сказала, что эти настройки были тщательно спроектированы, и что компания открыта к обратной связи. Она также подробно ответила на некоторые вопросы, но не разрешила публиковать эти ответы.

Google Maps ограничивает возможности, если вы не хотите делиться историей поиска



Google Maps, когда у вас нет связи

Открывая Google Maps, вы увидите кружочек в правом верхнем углу, символизирующий, что вы вошли в свою учётную запись Google. Это не обязательно. Вы можете выйти оттуда. Естественно, кнопка выхода спрятана, но её можно найти. Нажимаете на кружочек, настройки, прокрутить вниз, выйти из Google Maps.

К сожалению, Google Maps не позволит вам сохранять часто посещаемые места, если вы не зайдёте в учётку. Если вы пользуетесь картами, не входя в учётку, по нажатию на поисковую строку вы увидите кнопку устали печатать?, предлагающую вам войти, и склоняющую вас тем самым к тому, чтобы собирать больше ваших данных.

Google Maps могут настучать на вас



Google Maps Хронология

Ещё одна проблемная функция это Google Maps Хронология. Она показывает примерные места, где вы могли быть, и маршруты, которые вы могли пройти, на основании истории вашего местоположения. С её помощью можно посмотреть на свои маршруты перемещений, включая и транспорт, которым вы, вероятно, пользовались например, машина или велосипед. Очевидный её минус обо всех ваших передвижениях узнает Google, а также любой, кто войдёт в вашу учётную запись.

И речь не только о хакерах Google может также делиться данными с государственными агентствами, например, с полицией. На странице вопросов и ответов Google написано, что юридическая команда рассматривает каждый такой случай отдельно. Каждые шесть месяцев компания выпускает отчёт о прозрачности. Для 2020 года пока ничего нет. С июля по декабрь 2019 года Google получила 81 785 запросов, касающихся 175 715 учётных записей со всего мира, и в большинстве случаев выдала запрашиваемую информацию.


График запросов на раскрытие информации со всего мира, с 2009 по 2019

Если ваша "Хронология" на телефоне включена, она сохраняет информацию о том, куда вы идёте со своим устройством, даже если вы не используете какой-то из сервисов Google, как описано на странице поддержки. Это будет полезным, если вы потеряете телефон, однако превращает его в устройство для слежки.

Google Maps хочет знать ваши привычки



Выдуманные отзывы для примера

Отзывы на картах могут оказаться очень полезными, однако быстро поискав в них, можно обнаружить чувствительную информацию, бездумно оставленную самими пользователями. Приведу только один пример отзыва, который оставил пользователь, использующий, судя по всему, своё настоящее имя, о супермаркете в Берлине. Хожу туда два-три раза в неделю уже четыре годя подряд, закупиться для семьи или прогуляться после ужина. Стоит ли говорить, что делиться такой информацией со всем миром рискованно.

Google Maps часто просит пользователей поделиться своей оценкой заведения. Как вам Берлинские Бургеры? Помогите другим понять, чего ожидать предлагает приложение после того, как вы забираете еду себе на ужин. Кажется, что это простой и искренний вопрос, обращающийся к позитивному ощущению помощи другим людям. Но вся эта информация собирается в вашем профиле Google, и кто-то посторонний может понять, ходите ли вы в это заведение изредка (например, по праздникам), или же вы живёте поблизости.

Если вы пожалеете о своём отзыве, Google, по крайней мере, даёт вам возможность скрыть обзоры после их публикации. Неинтуитивный путь к этой возможности такой (в мобильном приложении Android): иконка профиля Ваш профиль Изменить профиль Настройки профиля и доступа прокрутить вниз Профиль с ограниченным доступом. После включения этой возможности следить за вашим профилем и вашими отзывами смогут только одобренные вами пользователи.

Google Maps не любит быть в офлайне



Google Maps в офлайне

Помните GPS-навигаторы? Возможно, они были неуклюжими и тяжеловесными, однако они служат хорошим напоминанием тому, что получать направления движения можно и без подключения к интернету. В других приложениях есть офлайн-навигация. Для Google Maps можно скачать карты, однако офлайн-навигация предлагается только для автомобилей. Маловероятно, что техногигант не может разобраться в том, как подсказывать направления пешеходам и велосипедистам без доступа к интернету.

Google притворяется, что делает всё это для вашего же блага


Предоставление полезных и осмысленных услуг основа деятельности Google, говорится на веб-сайта компании. Там же указано, что по этим же причинам для компании важно знать, где вы находитесь. Она утверждает, что использует эту информацию для всяких полезных вещей, типа безопасности и настроек языка и, конечно, продажи рекламы. Также Google продаёт рекламодателям возможность оценивать степень достижения кампанией цели (то есть, вас), и смотреть, как часто люди заходят в физические магазины
анонимно путём агрегации информации. Но только если вы этого захотите (или забудете отключить эту опцию).

Google Maps есть альтернативы, но не такие хорошие


Иногда для проблемных приложений есть альтернативы. Например, есть альтернатива WhatsApp, но не Google Maps. Настройки конфиденциальности у Apple Maps жёстче, но для Android таких карт нет. Приложения типа Here WeGo и OsmAnd тоже собирают информацию, а пользоваться ими не так угодно. Но если вы любите ходить пешком, предпочитая быть в офлайне, то OsmAnd и Maps.me, по крайней мере, покажут вам направления без подключения к интернету.
Подробнее..

Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS

15.03.2021 12:08:42 | Автор: admin


Привет, Хабр! Меня зовут Георгий Гигаури, я разрабатываю Android-приложение Delivery Club. Эта статья появилась после доклада на конференции Mobius 2020, где мы выступали вместе с Павлом Борзиковым. Для тех, кто любит видео, ищите его в конце статьи.

Почему мы вообще обратили внимание на Huawei-устройства? Всё началось с того, что Huawei теперь не может распространять свои устройства с сервисами Google Play. Да, они могут использовать ОС Android, так как это открытая операционная система, но чтобы распространять устройства с сервисами Google Play, необходимо иметь лицензию. К сожалению, Huawei не может получить её из-за разногласий между Китаем и США. Поэтому Huawei приходится разрабатывать свои собственные Mobile Services. Справедливости ради, они этим занимались уже давно, но теперь им приходится расширять кодовую базу, активно увеличивать количество сервисов.

Почему стоит обратить внимание на экосистему Huawei


Смартфоны Huawei очень популярны: в 2020 году в России они занимали почти 18% рынка (Рис.1), а в мире 11% (Рис.2), (источник). Huawei заявила, что более 490 млн человек в более чем в 170 странах мира пользуются AppGallery (источник). Поскольку аудитория у Huawei-устройств огромная, мы не можем это игнорировать и решили поддержать пользователей нашего приложения. Далее поэтапно рассмотрим, что же нужно сделать.


Рис.1


Рис.2

Этап 1: проверка наличия Services


Если у вас в приложении при входе есть проверка наличия Google Services, то придётся от этого отказаться, и проверять наличие соответствующих сервисов только по мере необходимости.

fun Context.getMobileServiceSource(): MobileServicesSource {    val googleApi = GoogleApiAvailability.getInstance()    if (googleApi.isGooglePlayServicesAvailable(this) == com.google.android.gms.common.ConnectionResult.SUCCESS) {        return MobileServicesSource.GOOGLE    }    val huaweiApi = HuaweiApiAvailability.getInstance()    if (huaweiApi.isHuaweiMobileServicesAvailable(this) == com.huawei.hms.api.ConnectionResult.SUCCESS) {        return MobileServicesSource.HMS    }    return MobileServicesSource.NONE}enum class MobileServicesSource {    GOOGLE,    HMS,    NONE}

Пример реализации: добавляем extension, возвращающий конкретный доступный сервис, а в момент использования функциональности проверяем, что на устройстве есть нужные нам сервисы.

Этап 2: карты


В приложении Delivery Club три основные страницы:

  • карта с местоположением клиента;
  • информация о ресторане, который будет готовить заказ, с указанием его местоположения на карте;
  • карта с отслеживанием местоположения курьера, везущего заказ.

На устройствах Huawei все эти карты не работают. Чтобы это исправить, можно просто заменить зависимости: вместо пакета com.google.android.gms использовать com.huawei.hms:





Конечно, есть нюансы, но мы уже сделали большую часть работы. Huawei сделала Maps SDK с контрактами, по большей части соответствующий Google Maps SDK. Однако у Google есть deprecated-методы, если вы их используете, то аналогов у Huawei может и не найтись. Например, для получения местоположения пользователя мы используем:

LocationServices.FusedLocationApi.getLastLocation(googleApiClien)

Такой подход считается deprecated, и если мы просто скопируем код для Huawei Maps и заменим зависимости, то работать не будет. Нужно поменять так:

LocationServices.getFusedLocationProviderClient().getLastLocation().addOnSuccessListener()

PolyUtil. Расшифровка с помощью Polyline


Теперь нам нужно отображать перемещение курьера. Для этого нам нужно расшифровывать строку, которая приходит с сервера. Для этого воспользуемся тем же алгоритмом от Google, с помощью которого строка кодировалась.



После расшифровки мы получили список координат курьера.

Реализация поддержки двух карт


Для поддержки нескольких карт необходимо создать обёртку для самих карт и для объекта.

Добавляем общий интерфейс, например, IMapWidget. Не забываем сделать общий класс для LatLng список координат курьера. У Google он лежит в пакете com.google.android.gms.maps.model.LatLng, а у Huawei в com.huawei.hms.maps.model.LatLng. Кладём список в PolyLineOptions и задаём ширину и цвет линии маршрута.

interface IMapWidget {    void animateCamera(...);    void setListener(OnMapEventListener listener);    void setMapPadding(...);    MapMarker addMarker(...);    ...}

Добавляем Custom Map View реализующего интерфейс IMapWidget:



Добавляем обёртку, которая позволяет нам указать, где мы хотим отрисовывать карту:

class MapWrapper : FrameLayout() {    fun setupMap(widget: IMapWidget) {        removeAllViews()        addView(widget as View)    }}

И в нужном месте вызываем метод добавления карты.

override fun onCreateView(...) {    ...    val map: IMapWidget = MapFactory.createMap()    viewMapWrapper.setupMap(map)    ...}

Такие обёртки класса нужно создать для всего: объектов, маркеров, PolyUtil, PolyLine и т.д.

Проблема: Карта не работает




Однажды нам сообщили о баге. Пользователь с устройством Huawei, находившийся в центре Москвы (Рис.3), открыл приложение, нажал на кнопку Переместиться на своё местоположение, и его перенесло в пустоту (Рис.4). Пользователь не видит, ни улиц, ни зданий, и он решил, что карта не работает.

Мы попробовали воспроизвести у себя эту проблему. И действительно попадали в неопределённое пространство. Когда попробовали чуть-чуть уменьшить масштаб карты, то оказалось, что мы попали в пригород Мариуполя (Рис.5). То есть из московских координат (55.819207, 37.493424) перенеслись в мариупольские (47.187447, 37.593137). Мы были в полном недоумении. Может быть, где-то у нас с числами что-то не то происходит. Возможно, происходят некие вычитания наших координат. Очень долго искали решение этой проблемы или хотя бы причину. Оказалось, что мы заменили импорты из Google-карт, и поэтому всё перестало работать. В конце концов мы добрались до paddingа.



Давайте быстро вспомним, что такое padding у карты. На (Рис.6) показан экран авторизации, карта занимает всю область экрана, даже под плашкой ручного ввода адреса. В таком случае, если мы не добавим padding карте, её центр будет находиться на месте зелёного треугольника, но мы хотим, чтобы он был в центре рабочей области карты. Padding сужает рабочую область (Рис.7). Не видимую, а именно рабочую. Карта будет по-прежнему занимать весь экран, но размер её рабочей области изменится. И когда вы будете переходить в новую координату, она будет принимать положение новой рабочей карты. Как оказалось, баг был именно из-за этого.

Первое решение: убрать padding. Как вы понимаете, такой вариант нам не подошёл. Мы хотели, чтобы всё отображалось красиво.

Второе решение проблемы: использовать анимированное перемещение, но с масштабированием.

val zoom = map.cameraPosition.zoommap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom))

При переходе с изменением масштаба карты всё работало правильно. Здорово! Мы подумали, что это нам подходит. На самом деле нет. У нас ещё есть третий экран, на котором нужно увеличивать карту относительно двух маркеров, чтобы zoom сам рассчитывался, поэтому мы не можем задать какое-то константное масштабирование. То есть такой вариант нам тоже не подошёл. Начали думать дальше и нашли новое решение.

Третье решение проблемы: вообще отказаться от анимации. Как оказалось, если вместо animateCamera сделать просто move, то перемещение будет происходить правильно. Так мы и сделали. Надеемся, в скором времени Huawei устранит эту проблему.

Этап 3: push-сервис


Идём дальше. На Huawei-устройства не приходят уведомления нашего приложения. Дело в том, что мы не можем получить токен. Давайте его получим. В Google мы получаем задачу и извлекаем токены так:

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->    if (task.isSuccessful) {        val token = task.result    }}

Наше решение:

class ImplementationHuaweiMessagingService : HmsMessageService() {    override fun onNewToken(token: String?) {        val commonApi = getComponentFactory().get(CommonApi::class.java)        commonApi.settingsManager().setPushToken(token)    }    override fun onMessageReceived(message: RemoteMessage?) {        message?.let {            val appManagersComponent = getComponentFactory().get(AppManagersApi::class.java)            appManagersComponent.pushManager().handle(it.dataOfMap)        }    }

Выглядит всё так же, как и с реализацией FirebaseMessagingService(), даже есть callbackи onNewToken и onMessageReceived. Однако без нюансов не обойтись. Случается, что на некоторых редких устройствах onMessageReceived вызывается в главном потоке, поэтому лучше не использовать здесь долго выполняющиеся задачи.

Получаем токены на Huawei:

val token = HmsInstanceId.getInstance(context)    .getToken(appId, com.huawei.hms.push.HmsMessaging.DEFAULT_TOKEN_SCOPE)public static final String DEFAULT_TOKEN_SCOPE = "HCM";

Обратите внимание, что метод выполняется в главном потоке. И для получения токена нужно отдельно реализовать поток. У Google такой подход уже считается устаревшим, возможно, Huawei придёт к тому же.

Мы можем вообще не использовать getToken, а прописать в манифесте автоматическую инициализацию или в коде методом setAutoInitEnabled() и всегда получать token в onNewToken (подробнее). Это решит ещё одну проблему: getToken в версиях EMUI ниже 10 вообще возвращает null.

<meta-data    android:name="push_kit_auto_init_enabled"    android:value="true"/>

Этап 4: Chrome Custom Tabs


Наше приложение при запуске регулярно вылетает с ошибкой ActivityNotFoundException. Чтобы от этого избавиться, нужно обработать отсутствие Chrome Tabs.

fun Context.openLink(url: String, customTabsSession: CustomTabsSession? = null): Boolean {    try {        openLinkInCustomTab(url, customTabsSession)        return true    } catch (throwable: Throwable) {        Timber.tag("Context::openLink").e(throwable, "CustomTabsIntent error on url: $url")    }    return openLinkInBrowser(url)}@Throws(Throwable::class)fun Context.openLinkInCustomTab(url: String, customTabsSession: CustomTabsSession? = null) {    CustomTabsIntent.Builder(customTabsSession)        .build()        .launchUrl(this, Uri.parse(url))}private fun Context.openLinkInBrowser(url: String): Boolean {    val intent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {        addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT)    }    if (intent.resolveActivity(packageManager) != null) {        startActivity(intent)        return true    }    return false}

Мы просто обернули openLinkInCustomTab() в try catch и в случае ошибки пытаемся открыть в браузере. Но бывает такого, чтобы на устройстве не было подходящего браузера, способного обработать наш неявный intent. Поэтому если метод openLinkInBrowser() возвращает false, мы открываем страницу в webview.

Этап 5: аналитика


Аналитика у Huawei похожа на Google Analytics. Покажу замену на примере Firebase. Сначала инициализируем: HiAnalytics.getInstance(context). Затем с помощью HAEventType.STARTCHECKOUT копируем все наши события из Firebase в отдельный файл huaweiAnalytics:

huaweiAnalytics.onEvent(name, bundle)

Системные параметры: HAParamType.PRICE, HAParamType.CURRNAME

Даже если у вас нет Firebase, добавить аналитику в Huawei очень просто. У них отличная документация, контракт соблюдается. Также у Huawei есть отличные инструменты для исследования аудитории.

Этап 6: crashlytics


Следующий инструмент, который нам тоже стало интересно попробовать, это Crashlytics от Huawei, которая называется AGConnectCrash. Она позволяет с минимальными усилиями собирать и анализировать информацию о падении приложения.

Инициализируем crashlytics:

AGConnectCrash.getInstance().enableCrashCollection(true)

Добавляем свои ключи и журналируем нужные события:

AGConnectCrash.getInstance().setUserId("testuser")AGConnectCrash.getInstance().log(Log.DEBUG, "set debug log.")AGConnectCrash.getInstance().log(Log.INFO, "set info log.")AGConnectCrash.getInstance().log(Log.WARN, "set warning log.")AGConnectCrash.getInstance().log(Log.ERROR, "set error log.")AGConnectCrash.getInstance().setCustomKey("stringKey", "Hello world")AGConnectCrash.getInstance().setCustomKey("booleanKey", false)AGConnectCrash.getInstance().setCustomKey("doubleKey", 1.1)AGConnectCrash.getInstance().setCustomKey("floatKey", 1.1f)AGConnectCrash.getInstance().setCustomKey("intKey", 0)AGConnectCrash.getInstance().setCustomKey("longKey", 11L)

Этап 7: покупки в приложении


Если в вашем приложении есть встроенные покупки, вы должны подписать согласие на передачу персональных данных, и отослать письмом в конверте или через курьера в московский офис Huawei. И только через пару дней они вам дадут доступ для реализации этой функциональности.

Всё очень похоже на реализацию Google. При запуске приложения запрашиваем все прошлые покупки пользователя:

fun getOwnedPurchases(    activity: Activity,    ownedPurchasesResultOnSuccessListener: OnSuccessListener<OwnedPurchasesResult>,    failureListener: OnFailureListener) {    val ownedPurchasesReq = OwnedPurchasesReq()    // priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription    ownedPurchasesReq.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    // To get the Activity instance that calls this API.    val task: Task<OwnedPurchasesResult> = Iap.getIapClient(activity)        .obtainOwnedPurchases(ownedPurchasesReq)    task.addOnSuccessListener(ownedPurchasesResultOnSuccessListener)        .addOnFailureListener(failureListener)}

Если какой-то товар был куплен, мы разблокируем его функциональность. Потом запрашиваем подробности по товарам, доступным для продажи цену и описание:

fun loadProduct(    context: Context,    productInfoResultOnSuccessListener: OnSuccessListener<ProductInfoResult>,    onFailureListener: OnFailureListener) {    // obtain in-app product details configured in AppGallery Connect, and then show the products    val iapClient: IapClient = Iap.getIapClient(context)    val task: Task<ProductInfoResult> = iapClient.obtainProductInfo(createProductInfoReq())    task.addOnSuccessListener(productInfoResultOnSuccessListener)        .addOnFailureListener(onFailureListener)}private fun createProductInfoReq(): ProductInfoReq {    val req = ProductInfoReq()    // 0: consumable ; 1: non-consumable ; 2: auto-renewable subscription    req.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    val productIds = ArrayList<String>()    productIds.add("PRODUCT_ID")    req.productIds = productIds    return req}

Когда пользователь кликает на товар, мы открываем страницу с оплатой. Она не такая красивая, как у Google, и не выезжает снизу.

fun gotoPay(activity: Activity, productId: String, type: Int) {    val client: IapClient = Iap.getIapClient(activity)    val task: Task<PurchaseIntentResult> = client.createPurchaseIntent(createPurchaseIntentReq(type, productId))    task.addOnSuccessListener { result ->        result?.let {            val status: Status = result.status            if (status.hasResolution()) {                try {                    status.startResolutionForResult(activity, PAY_RESULT_ARG)                } catch (exception: SendIntentException) {                    Timber.e(exception)                }            } else {                Timber.d("intent is null")            }        }    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Так как это Activity, мы передаём ему аргумент, по которому можно отловить OnActivityResult и понять, успешно ли прошла оплата и как закончилась транзакция:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {    super.onActivityResult(requestCode, resultCode, data)    if (resultCode == PAY_RESULT_ARG) {        val purchaseResultInfo: PurchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data)        when (purchaseResultInfo.returnCode) {            OrderStatusCode.ORDER_STATE_SUCCESS -> {                successResult(purchaseResultInfo)            }            OrderStatusCode.ORDER_STATE_CANCEL -> {            }            OrderStatusCode.ORDER_PRODUCT_OWNED -> {            }        }    }}

У нас есть специальные статусы: ORDER_SUCCESS, CANCEL, OWNED. Первый означает успешную оплату. Второй пользователь просто закрыл страницу без покупки, тогда мы обрабатываем этот callback и предлагаем скидку, чтобы уговорить на покупку. А третий статус означает, что товар уже куплен пользователем. Если товар разовый или подписочный, то на этом моменте нужно остановиться, в противном случае виртуально доставить покупку.

В случае успешной оплаты доставляем пользователю купленный товар:

private fun successResult(purchaseResultInfo: PurchaseResultInfo) {    val inAppPurchaseData = InAppPurchaseData(purchaseResultInfo.inAppPurchaseData)    val req = ConsumeOwnedPurchaseReq()    req.purchaseToken = inAppPurchaseData.purchaseToken    val client: IapClient = Iap.getIapClient(this)    val task: Task<ConsumeOwnedPurchaseResult> =        client.consumeOwnedPurchase(req)    task.addOnSuccessListener {        // Consume success    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Если не сделать доставку, то функциональность товара будет у пользователя заблокирована, а деньги возвращены. В Google Play Billing Library до третьей версии этого делать не нужно было, но потом Google тоже это добавил, и если мы не доставим товар, через 48 часов покупка отменится, а деньги вернутся пользователю. То есть в Huawei покупки реализованы как в третьей версии Google Play Billing.

Выводы


На реализацию поддержки Huawei-устройств не уйдёт много времени. Даже без реальных устройств вы сможете проверить работоспособность вашего приложение: у Huawei есть своя тестовая лаборатория с виртуальными устройствами наподобие Samsung Remote test lab. Количество пользователей быстро растёт, и бизнесу может оказаться выгодным вложиться в доработку продуктов, а отличная документация поможет разработчикам всё сделать быстро. Поддержка HMS активно отвечает на любые вопросы, если вы не сможете в документации что-то найти.

Видеозапись доклада с конференции Mobius 2020.

Полезные ссылки


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru