Пишем виджет, использующий API Яндекс.Метрики

    Не так давно Яндекс.Метрика анонсировала открытый API, с помощью которого можно получить доступ практически ко всем функциям Метрики из собственной программы.
    Сегодня я хочу немного рассказать об использовании этого API и о том, как на его основе создать простой widget для Android-устройств.


    Основы работы с API


    API Яндекс.Метрики построен по принципу REST. Все чем можно управлять через API, представлено ресуром: счетчики, цели, фильтры и т.п. Операции над ресурсами (чтение, удаление, изменение) совершаются посредством HTTP запросов к серверам API Я.Метрики, каждому типу операции соответствует свой HTTP метод:
    • GET: чтение содержимого
    • POST: добавление ресурса
    • PUT: изменение ресурса
    • DELETE: удаление ресурса

    Например, для получения списка счетчиков надо обратиться к ресурсу counters: GET /counters. Для получение информации об одном счетчике, обратиться к ресурсу по его идентификатору: GET /counter/{id}. Для удаления — обратиться к тому же ресурсу, но используя метод DELETE: DELETE /counter/{id}.

    Входные данные для REST вызовов и результаты могут кодироваться в двух форматах: XML и JSON. В дальнейшем мы будем использовать JSON, как более компактный и удобный для отображения на структуры, использующиеся в языках программирования, формат.

    Естественно, работа через API возможно только со счетчиками, к которым есть доступ из своего аккаунта Я.Метрики. Для идентификации владельца аккаунта используется OAuth авторизация. Устроена она очень просто. Разработчик, желающий воспользоваться API Я.Метрики, регистрирует свое приложение (см. инструкции) и получает идентификатор приложения. Зарегистрировав приложение, можно получить для него отладочный OAuth-токен и сразу же начать работу с API Метрики. Отладочный токен разрешит работу со счетчиками, доступными из аккаунта разработчика, зарегистрировавшего приложение. Токен надо передавать в каждом HTTP запросе, или как дополнительный параметр в URL (...&oauth_token=<acces_token>), или как заголовок в HTTP запросе Authorization: OAuth <access_token>.

    Чтобы приложение могло работать с произвольными счетчиками, а не только с доступными разработчику, нужен отдельный OAuth токен для каждого пользователя приложения. Как его можно получить:
    1. Наиболее безопасен способ, когда пользователь перенаправляется приложением на специальную страницу Яндекса, авторизуется на ней, и (если еще не сделал этого) дает приложению доступ к своим счетчикам. После этого пользователь перенаправляется обратно в приложение, причем в URL-е, на который идет перенаправление, содержится параметр с OAuth токеном для этого пользователя. Приложение должно прочитать этот параметр и запомнить токен. Детали этой процедуры описаны в руководстве.
    2. Приложения также могут получить токен, непосредственно запросив у пользователя логин и пароль, передав их OAuth серверу Яндекса, и получив токен в ответ. Этот способ менее безопасен: логин и пароль запрашиваются и передаются в явном виде.

    Благодаря использованию REST, простейшая работа с API (запросы на чтение) возможна непосредственно из браузера, без какого-либо кодирования. Например, этот запрос выдаст информацию о счетчике в демо-аккаунте API Я.Метрики. Такой запрос выдаст отчет по посещаемости счетчика.

    Несмотря на простоту REST интерфейса, для полноценного использования API в программе, написанной на языке высокого уровня, необходимо совершить довольно много телодвижений: формирование URL-ов запросов, отправка HTTP запросов и получение результатов, формирование и парсинг JSON, обработка ошибок и т.д. Чтобы упростить жизнь, я создал готовую библиотеку для работы с API Я.Метрики на языкe Java: Metrika4j. Код библиотеки распространяется под Apache License, вы можете свободно изменять и дополнять его под ваши требования. Далее я расскажу, как с помощью этой библиотеки создать widget для Android устройств, отображающий посещаемость сайта.

    Metrika4j


    Metrika4j позволяет работать с Яндекс.Метрикой, оперируя привычными Java разработчику понятиями (классы, вызовы функций, типы) и не задумываясь о низкоуровневой работе с HTTP и JSON. Центральный интерфейс библиотеки — MetrikaApi. В нем есть методы для работы с главными сущностями Метрики: счетчиками и отчетами. Работа с дополнительными сущностями (цели, фильтры и т.п.) вынесена в отдельные мини-API, получаемые из главного класса MetrikaApi.

    Структура методов примерно соответствует структуре REST-вызовов API Я.Метрики. Аргументы, передаваемые в REST-вызов, соответствуют аргументам, передаваемым в методы API (кроме API работы с отчетами, о котором мы расскажем отдельно).
    Metrika4j поддерживает «из коробки» две библиотеки для работы с JSON: Jackson JSON processor и org.json. Для Jackson поддерживается 100% функциональности, для org.json — минимум, достаточный для работы с отчетами: чтение списка счетчиков и получение отчетов. Библиотека org.json встроена в Andriod API, поэтому её использование удобно для Android приложений. При необходимости, разработчик может воспользоваться любой другой JSON-библиотекой, имплементировав с её помощью интерфейсы JsonMapper и JsonObject.

    Использование Metrika4j

    Сначала необходимо создать экземпляр MetrikaApi c помощью ApiFactory. При создании надо передать OAuth-токен пользователя:

    // Создаем экземпляр API, использующий демо-токен API Метрики и Jackson JSON processor
    MetrikaApi api = ApiFactory.createMetrikaAPI(
        "05dd3dd84ff948fdae2bc4fb91f13e22", new JacksonMapper());


    Далее с помощью созданного экземпляра можно выполнять любые операции:

    // Получаем список счетчиков в текущем аккаунте
     Counter[] myCounters = api.getCounters();
     
    // Создание счетчика
    Counter newCounter = new Counter();
    newCounter.setSite("mysite.ru");
    newCounter.setName("Мой сайт");
    Counter createdCounter = api.createCounter(newCounter);
    // В createdCounter содержится новый счетчик, загруженный из Метрики, со всем значениями полей,
    // проставленными Метрикой, например Id
    System.out.println(createdCounter.getId());
     
    // Удаление счетчика
    api.deleteCounter(createdCounter.id);
    


    Работа с отчетами

    Через API доступны почти все отчеты, существующие в Метрике. Набор доступных отчетов содержится в классе Reports. Для построения выбранного отчета надо вызвать метод MetrikaApi.makeReportBuilder(Reports report, int counterId), который вернет специальный объект — построитель отчета ReportBuilder. В построителе отчета надо задать требуемые параметры отчета (временной интервал, сортировку и т.п.). Когда все параметры будут установлены, вызывайте метод ReportBuilder.build(), который отправит запрос HTTP запрос к API Метрики и вернет отчет.

    // Создаем построитель отчета "популярное содержимое" для счетчика с id=2138128
    ReportBuilder builder = api.makeReportBuilder(Reports.contentPopular, 2138128);
     
    // Задаём параметры отчета (отчет за неделю) и строим отчет
    Report report = builder.withDateFrom(MetrikaDate.yesterday())
        .withDateTo(MetrikaDate.today())
        .build();
    


    Отчет возвращается в виде объекта Report, представляющего собой таблицу с результатами, плюс некоторая дополнительная информация (итоги, интервал дат, к которым относится отчет и т.п.). Каждая строка таблицы результатов — объект ReportItem, данные из которого можно получить одним из методов getXXX(String fieldName) (аналогично получению значений из ResultSet при использовании JDBC). Имена полей и возвращаемые дополнительные данные надо уточнять для каждого отчета в документации на API Яндекс.Метрики.

    // Вытаскиваем результаты из отчета
    ReportItem[] items = report.getData();
    for (ReportItem item : items) {
          System.out.printf("pageViews: %4d, url: %s", item.getInt("page_views"), item.getString("url"))
              .println();
    }
    


    Более подробное описание работы с Metrika4j содержится в Javadoc.

    Виджет для Android


    Имея в своем распоряжении такие мощные инструменты, как API Я.Метрики и Metrika4j, можно решать любые задачи, вплоть до создания альтернативных пользовательских интерфейсов к Яндекс.Метрике. Но в рамках этой статьи мы ограничимся более скромной целью: создадим виджет для Android, который будет показывать текущую посещаемость сайта. Исходный код виджета доступен на GitHub в проекте MetrikaWidget. Так же, как и код Metrika4j, он свободно распространяется с минимумом лицензионных ограничений.

    OAuth авторизация

    Начнем с создания MetrikaApi и авторизации. Приложение работает только с одним аккаунтом, поэтому экземпляр MetrikaApi можно сделать Singleton-ом. Его код содержится в классе Globals.

    private static MetrikaApi api;
     
    public static synchronized MetrikaApi getApi(Context context) {
        if (api == null) {
            // Получаем сохраненный OAuth token из SharedPreferences
            String token = getOAuthToken(context); 
            if (token == null) {
                throw new AuthException();
            } else {
                // Используем библиотеку org.json, встроенную в Android
                api = ApiFactory.createMetrikaAPI(token, new OrgJsonMapper());
            }
        }
        return api;
    }
    


    Если приложение запускается в первый раз и OAuth token-а еще нет в SharedPreferences, надо получить его. Для этого переходим на URL запроса токена:

    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://oauth.yandex.ru/authorize?response_type=token&client_id=1359488e196b4bfa92615d0885b106d4"));
    startActivity(intent);
    


    Для пользователя это будет выглядеть, как открывшаяся страница «Приложение запрашивает доступ к вашим данным на Яндексе». Предположим, что пользователь успешно авторизовался и разрешил доступ. Дальше начинается интересное: как передать токен из веб-интерфейса обратно в наше приложение? Чтобы сделать это, надо зарегистрировать одну из активностей в приложении, как обработчик специфического для нашего приложения протокола (специфического — чтобы наш обработчик не пересекся с другими приложениями). В AndroidManifest.xml укажем следующее:

    <activity android:name="ru.metrikawidget.AuthTokenActivity" android:label="OAuth">
        <intent-filter>
            <action android:name="android.intent.action.VIEW"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <category android:name="android.intent.category.BROWSABLE"/>
            <data android:scheme="metwd" android:host="oauthtoken"/>
        </intent-filter>
    </activity>
    


    Мы зарегистрировали AuthTokenActivity, как обработчик протокола «metwd» для псевдохоста «oauthtoken». Теперь достаточно указать Callback URI metwd://oauthtoken в настройках зарегистрированного приложения, и после успешной OAuth-авторизации пользователя будет вызываться AuthTokenActivity. В этой активности нужно распарсить полученную строку и сохранить токен в SharedPreferences:

    String fragment = getIntent().getData().getFragment();
    String[] parts = fragment.split("\\&");
    for (String part : parts) {
        if (part.startsWith("access_token=")) {
            String token =  part.substring(param.length(), part.length());
            // Запоминаем полученный токен в preferences
            getSharedPreferences(Globals.PREF_FILE, 0)
                 .edit()
                 .putString(Globals.PREF_TOKEN, token)
                 .commit();
        }
    }
    


    Получение данных

    Код получения данных для виджета предельно прост:

    MetrikaApi api = Globals.getApi(context);
    // Собственно запрос к Metrika API
    Report report = api.makeReportBuilder(Reports.trafficSummary, counterId)
            .withDateFrom(new MetrikaDate())
            .withDateTo(new MetrikaDate())
            .build();
    return new Result(report.getTotals().getInt("visits"));
    


    Нюанс в том, что этот код нельзя выполнять «в лоб». Сервера API Метрики отвечают довольно быстро, а вот сам HTTP запрос может доооолго идти до сервера и обратно по медленным и ненадежным каналам мобильной связи. В результате виджет, ожидающий ответ, «зависнет» с точки зрения Android OS, и будет выдано окошко, предлагающее аварийно завершить подвисшее приложение. Это явно не то, что нам нужно. Поэтому все запросы к API Метрики выполняются асинхронно, с помощью класса AsyncTask. Вот упрощенный код загрузки списка счетчиков (полная версия — в классе WidgetSetupActivity):

    private class CountersLoadTask extends AsyncTask<Void, Void, Counter[]> {
        private ProgressDialog progressDialog;
     
        protected void onPreExecute() {
            progressDialog = ProgressDialog.show(...);
        }
     
        protected void onPostExecute(Counter[] counters) {
            progressDialog.dismiss();
            counterList.addAll(Arrays.asList(counters));
            // Уведомляем адаптер, чтобы он перерисовал список счетчиков
            listAdapter.notifyDataSetChanged(); 
        }
     
        protected Counter[] doInBackground(Void... voids) {
            return Globals.getApi(WidgetSetupActivity.this).getCounters();
        }
    }
    


    Для виджетов работа с данными немного сложнее. Начнем с того, что виджет-провайдер, получающий события «пора обновить виджет», в терминологии Android является broadcast receiver-ом. Жизненный цикл broadcast receiver-а короток: он обрабатывает событие, после чего немедленно умирает. Если стартовать из обработчика события поток, то смерть receiver-a может наступить раньше, чем закончит работу поток: печаль. Android developer guide настоятельно рекомендует пользоваться для таких случаев сервисами. Так и сделаем: виджет-провайдер (MetrikaWidgetProvider) получает события и передаёт их для обработки в UpdateService. UpdateService, в свою очередь, использует асинхронную загрузку данных через AsyncTask (в противном случае опять получим окошко «Application Not Responding» при длительном ожидании ответа от API).

    Отображение виджета

    Работа с виджетами в Android — отдельная большая тема, выходящая за рамки этой статьи и хорошо освещенная как в руководстве разработчика Android, так и в дополнительных статьях. Поэтому расскажу кратко, а за деталями отошлю к исходному коду классов MetrikaWidgetProvider и UpdateService.

    Виджет может находиться в трех состояниях: «данные получены», «обновление данных» и «нет связи». Обновление виджета происходит по таймеру или при клике на виджет.
    • Данные получены — штатное состояние виджета, в котором он отображает количество визитов на сайте за сегодняшний день. В этом состоянии виджет отображает стандартную столбчатую диаграмму — «радугу», которую видно в шапке интерфейса Яндекс.Метрики
    • Обновление данных — промежуточное состояние, сделанное исключительно для удобства пользователя, чтобы он получил визуальный feedback от клика на виджет. Виджет переходит в него перед отправкой запроса к API Метрике, и выходит после завершения запроса. В этом состоянии отображается инвертированная «радуга»
    • Нет связи — состояние, указывающее на то, что актуальных данных получить не удалось. Отображается «радугой» с уменьшенной насыщенностью, почти серого цвета.


    Если виджет может находиться в состоянии «нет связи», логично было бы обновлять его при появлении этой самой связи, чтобы пользователь сразу увидел актуальные данные. Для этого виджет-провайдер подписывается на системные события, связанные с изменением статуса сетевого интерфейса:

    <receiver android:name="ru.metrikawidget.MetrikaWidgetProvider">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            <action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"/>
            <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
        </intent-filter>
        ...
    </receiver>
    


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

    if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
        // Изменилось состояние подключения к Internet
        NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
        if (info != null && info.isConnected() && !info.isRoaming()) {
            // Если появилась связь, обновляем виджеты, которые могли быть в состоянии "offline"
            ...
        }
    }
    


    Установка виджета


    Виджет может быть собран из исходного кода, находящегося в проекте MetrikaWidget.

    Вы не Android-разработчик, но хотите пользоваться виджетом? Нет проблем — можно загрузить готовый виджет. Релиз с Android Market или debug build c GitHub.

    image

    После установки у вас также появится приложение «Я.Метрика», которое на самом деле является мини-инструкцией к виджету.
    Поделиться публикацией

    Комментарии 12

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Что-то при попытке авторизации пишет Error 400, Required parameter missing: client_id
          0
          Такое бывает, если вы еще не авторизованы в Яндексе в момент, когда даете приложению разрешение на доступ к своим данным (Яндексовый OAuth теряет параметр client_id во время авторизации). Попробуйте сначала авторизоваться в мобильном браузере на любом сервисе Яндекса, а потом установить виджет.
            0
            Попробовал.
            Теперь предлагает разрешить доступ. Нажимаю на «confirm», долго думает и потом выдает что страница недоступна.

            Использую браузер Dolpin, может стоит попробовать родным?
              0
              Попробуйте сначала или совсем прибить браузер, или закрыть в нем все вкладки — чтобы не осталось открытых страниц, относящихся к экранам авторизации, а потом пройти процедуру выдачи разрешения.
              С Dolphin не тестировал, возможно с ним OAuth работает хуже.
          0
          Все прекрасно встало — LG P500 :-) Спасибо Вам!
            0
            Афигенно удобно. Установил.
              –1
              Ждем приложения на iPhone.
                0
                Отличная штука, спасибо. Только может по клику на виджет открывать браузер с подробной статистикой?
                  0
                  Спасибо, очень красиво и удобно.
                    0
                    Знаете, есть такая китайская поговорка: «Если долго сидеть на берегу реки, то рано или поздно мимо проплывет труп твоего врага».
                    К чему это я? Я уже пол-года хотел написать такой виджет :)
                    А можно еще виджет с остатком денег на директе? Пожалуйста!!!
                      0
                      Спасибо дважды и за материал, и за виджет, которого реально многим не хватало (это даже по комментариям видно).

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

                      Самое читаемое