Приложение "Погода". Получаем погоду в сервисе. BroadcastReceiver. SharedPreferences.

Урок #35

Урок 35 — Приложение "Погода". Получаем погоду в сервисе. BroadcastReceiver. SharedPreferences.

На самом деле, было бы хорошо подгружать погоду в сервисе — таким образом мы можем периодически подгружать её в фоне, а также показывать информацию в уведомлении.

Получаем геолокацию в сервисе

В первую очередь перенесём код запроса геолокации из MainActivity в наш сервис.

Запрос разрешений останется в Activity, но вся работа, связанная с гео будет находиться в сервисе.

Кроме того, удалим весь лишний код из сервиса. В итоге получим это:

public class WeatherService extends Service {

    private static final String TAG = "WeatherService";

    private LocationManager locationManager;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        Notification notification = createNotification();
        startForeground(1, notification);

        setupLocation();
    }

    @Override
    public void onDestroy() {
        locationManager.removeUpdates(locationListener);

        super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }


    /**
     * Создаём уведомление
     */
    private Notification createNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.drawable.ic_notification);
        builder.setContentTitle("Title");
        builder.setContentText("Text");
        builder.setOngoing(true);

        Intent mainIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(
                this,
                0,
                mainIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
        );

        builder.setContentIntent(pendingIntent);

        return builder.build();
    }

    /**
     * Подписываемся на обновления гео
     */
    private void setupLocation() {
        // Получаем LocationManager
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

        // Получаем лучший провайдер
        Criteria criteria = new Criteria();

        String bestProvider = locationManager.getBestProvider(criteria, true);

        Log.v(TAG, "Best provider: " + bestProvider);


        if (bestProvider != null) {

            // На всякий случай проверим, не убрал ли пользователь разрешение на ГЕО
            if (ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED

                    && ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                return;
            }

            // Получаем последнюю доступную позицию
            Location lastKnownLocation = locationManager.getLastKnownLocation(bestProvider);

            Log.v(TAG, "Last location: " + lastKnownLocation);


            // Подписываемся на обновления
            locationManager.requestLocationUpdates(
                    bestProvider, // провайдер
                    0, // мин. время
                    0, // мин. расстояние
                    locationListener
            );
        }
    }


    /**
     * Слушатель для обновления гео
     */
    private final LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            Log.v(TAG, "Location changed: " + location);

        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            Log.v(TAG, "Status changed: " + provider + ", status: " + status);
        }

        @Override
        public void onProviderEnabled(String provider) {
            Log.v(TAG, "Provider enabled: " + provider);
        }

        @Override
        public void onProviderDisabled(String provider) {
            Log.v(TAG, "Provider disabled: " + provider);
        }
    };
}

В MainActivity добавим новый метод, который будет запускать сервис:

private void startWeatherService() {
    Intent intent = new Intent(this, WeatherService.class);
    startService(intent);
}

Будем вызывать его в checkAndRequestGeoPermission() и onRequestPermissionsResult():

private void checkAndRequestGeoPermission() {
    int permissionCheck = ContextCompat.checkSelfPermission(this,
            Manifest.permission.ACCESS_FINE_LOCATION);
    if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                REQUEST_CODE_LOCATION_PERMISSION);
    } else {
        startWeatherService();
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE_LOCATION_PERMISSION) {
        if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            startWeatherService();
        } else {
            // Нет гео
            // Попробуем показать ещё раз
            checkAndRequestGeoPermission();
        }
    }
}

Попробуйте запустить приложение и увидите, что мы успешно получаем геолокацию из сервиса:

11-16 23:58:01.671 4552-4552/com.skillberg.weather2 V/WeatherService: Best provider: gps
11-16 23:58:01.678 4552-4552/com.skillberg.weather2 V/WeatherService: Last location: Location[gps 37.421998,-121.084000 acc=20 et=+18s4ms alt=0.0 {Bundle[EMPTY_PARCEL]}]

Делаем запросы к API в сервисе

Добавим инстанс Api в сервис:

private final Api api = ApiFactory.createApi();

А ещё нам пригодится поле для текущей погоды:

@Nullable
private CurrentWeather currentWeather;

Создадим новый метод:

private void getCurrentWeather(double latitude, double longitude) {
}

В нём создадим уже знакомый нам запрос к API:

Call<CurrentWeather> call = api.getCurrentWeather(
        latitude,
        longitude,
        Constants.API_KEY,
        Constants.DEFAULT_UNITS
);

Однако, в отличие от примера с тестами, мы будем делать асинхронный вызов. Для этого нам понадобится метод enqueue(). Он принимает в качестве параметра Callback с методами:

  • onResponse() — будет вызван, если метод выполнился успешно, либо неуспешно с HTTP-ошибкой.
  • onFailure() — будет вызван, если произошла ошибка сети.

Таким образом, код будет выглядеть так:

call.enqueue(new Callback<CurrentWeather>() {
     @Override
     public void onResponse(@NonNull Call<CurrentWeather> call, @NonNull Response<CurrentWeather> response) {
         if (response.isSuccessful()) {
             CurrentWeather currentWeather = response.body();

             Log.i(TAG, "Got weather: " + currentWeather);

             WeatherService.this.currentWeather = currentWeather;

             setCurrentWeather();
         } else {
             Log.e(TAG, "Failed to get current weather. Code: " + response.code());
         }
     }
     @Override
     public void onFailure(@NonNull Call<CurrentWeather> call, @NonNull Throwable t) {
         Log.e(TAG, "Failed to get current weather: " + t.getMessage());
     }
 });

Также добавим новый метод setCurrentWeather(), который будет вызываться, когда мы получили новую погоду:

private void setCurrentWeather() {
    Notification notification = createNotification();
    startForeground(1, notification);
}

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

<string name="title_updating_weather">Updating</string>
<string name="text_updating_weather">Updating weather…</string>

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

<string name="title_current_weather">%1$d° — %2$s</string>
<string name="text_current_weather">Min: %1$d°, max: %2$d°</string>

А такой — когда получили погоду.

  • В заголовке будут температура и город.
  • В тексте будет минимальная и максимальная температура.

Ну и, конечно, обновим код показа уведомления:

private Notification createNotification() {
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
    builder.setSmallIcon(R.drawable.ic_notification);

    if (currentWeather == null) {
        builder.setContentTitle(getString(R.string.title_updating_weather));
        builder.setContentText(getString(R.string.text_updating_weather));
    } else {
        builder.setContentTitle(getString(
                R.string.title_current_weather,
                (int) currentWeather.getMain().getTemp(),
                currentWeather.getCityName()));
        builder.setContentText(getString(
                R.string.text_current_weather,
                (int) currentWeather.getMain().getMinTemp(),
                (int) currentWeather.getMain().getMaxTemp()
        ));
    }
    builder.setOngoing(true);

    // ...

И, конечно же, обновим информацию о погоде в методе setupLocation() сразу после получения последней геопозиции:

Location lastKnownLocation = locationManager.getLastKnownLocation(bestProvider);
Log.v(TAG, "Last location: " + lastKnownLocation);
getCurrentWeather(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude());

Запустите приложение и увидите, что погода получена успешно:

Текущая погода в уведомлении
Текущая погода в уведомлении

Увеличиваем интервал получения погоды

Как вы помните, в запросе обновления геопозиции мы указали нули в качестве минимального времени и расстояния.

Это не очень хорошая практика, так как нам не нужна такая точность. Нам достаточно изменения позиции на 10 километров и минимального интервала в 60 минут:

locationManager.requestLocationUpdates(
        bestProvider, // провайдер
        TimeUnit.HOURS.toMillis(1), // мин. время
        10000, // мин. расстояние
        locationListener
);

Кроме того, не забудьте добавить получение погоды в метод onLocationChanged():

@Override
public void onLocationChanged(Location location) {
    Log.v(TAG, "Location changed: " + location);
    getCurrentWeather(location.getLatitude(), location.getLongitude());
}

BroadcastReceiver

В Android существует механизм BroadcastReceiver, позволяющий обмениваться сообщениями между приложениями.

Принцип действия таков:

  • Приложение подписывается на определённые сообщения.
  • Другое (или это же) приложение отправляет сообщение, используя метод sendBroadcast().
  • Когда приходит сообщение, вызывается метод onReceive() в BroadcastReceiver.

Существует два способа регистрации ресивера:

  • В AndroidManifest.xml.
  • В рантайме, например, в Activity.

У каждого Broadcast-сообщения есть так называемый Action — уникальный идентификатор. Есть предопределённые Action, но вы можете создать свой Action.

Регистрация в манифесте

К примеру, нам нужно запустить приложение, как только система загрузилась. Для этого нужно подписаться на android.intent.action.BOOT_COMPLETED.

Давайте создадим BroadcastReceiver, который будет вызываться при запуске системы:

public class BootReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

    }

}

В методе onReceive() мы запустим сервис:

if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
    context.startService(new Intent(context, WeatherService.class));
}

Зачем мы проверяем Action? Дело в том, что в некоторых случаях система может вызвать BroadcastReceiver для другого Action. Всегда проверяйте, что произошло именно то событие, которое ждёте!

Обратите внимание: BroadcastReceiver предназначен для выполнения очень краткосрочных операций (не более нескольких секунд). Если вам нужно выполнить длительную операцию, запускайте сервис, иначе система убьёт BroadcastReceiver. Кроме того, код будет выполнен на UI-потоке.

Осталось зарегистрировать BroadcastReceiver в манифесте:

<receiver android:name=".BootReceiver">
</receiver>

Внутрь этого тега добавьте специальный тег <intent-filter> — он указывает системе, по каким критериям система должна вызвать наш BroadcastReceiver:

<intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>

Кроме того, чтобы система вызвала наш ресивер, потребуется разрешение:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Готово! Запустите приложение, перезагрузите устройство, и вы увидите, как приложение запустилось!

Обратите внимание: в последних версиях Android очень сильно ограничили список системных экшенов, которые можно получить при регистрации в манифесте. Ознакомьтесь со списком тех Action-ов, которые можно регистрировать в манифесте. Все остальные нужно регистрировать в рантайме.

Обратите внимание: ни один BroadcastReceiver не будет вызван, если пользователь ни разу не запускал приложение после установки.

Регистрация в рантайме

Когда сервис получил погоду, нужно отправить информацию о погоде в MainActivity. Хороший способ сделать это — отправить Broadcast.



Продолжение доступно на платных тарифах

А вместе с ним — проверка домашних заданий нашими менторами.

Это совсем недорого — всего от 440 ₽ в месяц!



ВЫБРАТЬ ТАРИФ

Курсовая работа

После этого урока нужно выполнить курсовую работу.



Вход

Войдите, чтобы пользоваться всеми преимуществами.
Это займёт всего пару секунд!

или