1. Что такое Live Activities?

Для начала стоит разобраться, что же такое Live Activities и как эту концепцию видит Apple.

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

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

Несомненно, это невероятно удобно. Однако, к сожалению, не каждая платформа предлагает такую функциональность по умолчанию. Изначально Live Activities были эксклюзивной фишкой Apple и реализовывались через фреймворк ActivityKit. Но эта часть про Android, а он не предоставляет аналогичный механизм из коробки.

Здесь на помощь приходит RemoteViews — класс в Android, который описывает иерархию представлений, способную отображаться в другом процессе. Эта иерархия создается на основе XML-разметки, а RemoteViews предоставляет базовые операции для изменения содержимого этих представлений. Проще говоря, это удобный инструмент для динамического обновления контента уведомлений, позволяющий изменять элементы UI, такие как текст, индикатор прогресса и изображения, в реальном времени.

Но кто дает теорию без примера?) Для наглядности разберем процесс создания такой функциональности на моковых данных. Цель, которую я себе поставил будет выглядеть как-то так:

Превью того, ради чего мы тут собрались
Превью того, ради чего мы тут собрались

2. Создаем свой аналог Live Activity

Этап 1. Создаем данные
Для начала необходимо определить, какие данные мы будем использовать в этих уведомлениях. Поэтому логично начать с создания отдельного класса, который будет ответственен за управление этими данными.

class LiveActivityModel {
  int stage;
  int minutesToDelivery;
  int stagesCount;

  LiveActivityModel({
    required this.stage,
    required this.minutesToDelivery,
    required this.stagesCount,
  });

  factory LiveActivityModel.fromJson(Map<String, dynamic> json) {
    return LiveActivityModel(
      stage: json['stage'] as int,
      minutesToDelivery: json['minutesToDelivery'] as int,
      stagesCount: json['stagesCount'] as int,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'stage': stage,
      'minutesToDelivery': minutesToDelivery,
      'stagesCount': stagesCount,
    };
  }
}

Этап 2. Создаем связи

Для настройки механизма связи между Flutter-приложением и нативной Android-частью необходимо создать MethodChannel. Это позволит отправлять команды из Flutter в нативное приложение и получать ответы от Android.

Для управления уведомлениями создадим класс LiveActivityAndroidService, который будет содержать все методы для взаимодействия с нативной частью приложения. В этом классе определим методы для запуска, обновления, завершения и окончания уведомлений.

Кроме того, данные сразу будем преобразовывать в JSON, чтобы в Android-части можно было обращаться к ним по ключам.

class LiveActivityAndroidService {
  // Важно использовать везде одно название!
  final MethodChannel _method =
      const MethodChannel("flutterAndroidLiveActivity");

  Future<void> startNotifications({required LiveActivityModel data}) async {
    try {
      await _method.invokeMethod("startDelivery", data.toJson());
    } on PlatformException catch (e) {
      throw PlatformException(code: e.code);
    }
  }

  Future<void> updateNotifications({required LiveActivityModel data}) async {
    try {
      await _method.invokeMethod("updateDeliveryStatus", data.toJson());
    } on PlatformException catch (e) {
      throw PlatformException(code: e.code);
    }
  }

  Future<void> finishNotifications({required LiveActivityModel data}) async {
    try {
      await _method.invokeMethod("finishDelivery", data.toJson());
    } on PlatformException catch (e) {
      throw PlatformException(code: e.code);
    }
  }

  Future<void> endNotifications({required LiveActivityModel data}) async {
    try {
      await _method.invokeMethod("endNotifications");
    } on PlatformException catch (e) {
      throw PlatformException(code: e.code);
    }
  }
}

Чтобы не только Flutter знал, как обрабатывать эти запросы, нужно уведомить и Android. Для этого мы переопределим метод configureFlutterEngine в Android-части приложения(android\app\src\...\MainActivity.kt), где свяжем Flutter с соответствующими функциями в нативной части.

class MainActivity : FlutterActivity() {
    // Тот же канал, который мы указывали в нашем классе для методов  
    private val CHANNEL = "flutterAndroidLiveActivity"
    
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "startDelivery") {
                // ...Обработка вызова метода "startDelivery"
            } else if (call.method == "updateDeliveryStatus") {
                // ...Обработка вызова метода "updateDeliveryStatus"
            } else if (call.method == "finishDelivery") {
                // ...Обработка вызова метода "finishDelivery"
            } else if (call.method == "endNotifications") {
                // ...Обработка вызова метода "endNotifications"
            }
        }
    }
}

Сами того не замечая, мы уже сделали треть для разработки нашего "Live Activity". Теперь самая интересная часть :>

Этап 3. Плавное погружение в натив

Чтобы заложить каркас нашего самого дефолтного уведомления нам нужно его создать. Файл должен находиться в android\app\src\main\res\layout с расширением xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">
    <!-- Текст со статусом заказа -->
    <TextView
        android:id="@+id/order_status"  
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Your order is being processed"  
        android:textSize="16sp"
        android:textColor="#000000"
        android:layout_marginBottom="8dp" />
    <!-- Текст со этапом заказа -->
    <TextView
        android:id="@+id/stage_status"  
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Stage status 1/4"  
        android:textSize="16sp"
        android:textStyle="bold"
        android:shadowColor="#AA000000"
        android:textColor="#000000"
        android:layout_marginBottom="8dp"
        android:gravity="start" />
    <!-- Картинка (иконка этапа доставки) -->
    <ImageView
        android:id="@+id/image_stage"  
        android:layout_width="wrap_content"
        android:layout_height="100dp" 
        android:layout_marginBottom="8dp"
        android:src="@drawable/stage1"  
    />
</LinearLayout>

Как было упомянуто ранее всё наше уведомление, по сути своей - это динамически меняющийся контент XML-разметки. Поэтому очень хорошо, если вы его знаете. Если нет, то гугл чатгпт в помощь) Тут я разберу ключевые моменты:

android:id (для текста) и android:src (для изображения) — это два ключевых атрибута, с которыми мы будем работать почти всегда. Они создают связь между RemoteViews и нашей разметкой, позволяя динамически обновлять содержимое уведомлений и взаимодействовать с элементами интерфейса. Если с текстом все просто — передаем строку, и она сразу отображается, — то с изображениями ситуация немного сложнее. Все используемые изображения нужно заранее разместить в android/app/src/main/res/drawable, чтобы с ними можно было работать в уведомлениях. При этом важно учитывать рекомендации Android:

Android поддерживает растровые файлы следующих форматов: PNG (предпочтительно), WEBP (предпочтительно, требуется уровень API 17 или выше), JPG (допустимо), GIF (не рекомендуется).

Этап 4. Самый натив :)

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

/// Для версии андроида >= 8.0
@RequiresApi(Build.VERSION_CODES.O)
class LiveActivityManager(private val context: Context) {
    
     // Кастомная разметка уведомлений, связанная с именем пакета и XML-разметкой
    private val remoteViews = RemoteViews("com.example.live_acticity_article", R.layout.live_notification)

    // Уникальный идентификатор уведомления для его отображения и обновления
    private val notificationId = 100

    // Канал уведомлений с высоким приоритетом для важных уведомлений
    private val channelWithHighPriority = "channelWithHighPriority"

    // Канал уведомлений с обычным приоритетом для менее важных уведомлений
    private val channelWithDefaultPriority = "channelWithDefaultPriority"

    // Переменная для открытия MainActivity при взаимодействии с уведомлением
    private val pendingIntent = PendingIntent.getActivity(
        context, 
        200,
        Intent(context, MainActivity::class.java).apply { 
            flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 
        },
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    // Сервис для управления уведомлениями на устройстве
    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    init {
        createNotificationChannel(channelWithDefaultPriority)
        createNotificationChannel(channelWithHighPriority, true)
    }

    // Функция для создания каналов уведомлений
    private fun createNotificationChannel(channelName: String, importanceHigh: Boolean = false) {
        val importance = if (importanceHigh) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_DEFAULT
        val existingChannel = notificationManager.getNotificationChannel(channelName)
        if (existingChannel == null) { 
            val channel = NotificationChannel(channelName, "Delivery Notification", importance).apply {
                setSound(null, null)
                vibrationPattern = longArrayOf(0L)
            }
            notificationManager.createNotificationChannel(channel)
        }
    }

    // 1 стадия - Заказ оформлен
    private fun onFirstNotification(): Notification {
        return Notification.Builder(context, channelWithHighPriority)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("Live Notification - Your order has been processed")
            .setContentIntent(pendingIntent)
            .setWhen(3000)
            .setOngoing(true)
            .setCustomBigContentView(remoteViews)
            .build()
    }
    
    // 2 стадия - Заказ начал собираться
    private fun onGoingNotification(): Notification {
        return Notification.Builder(context, channelWithDefaultPriority)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("Live Notification - Your order is being collected")
            .setContentIntent(pendingIntent)
            .setOngoing(true)
            .setCustomBigContentView(remoteViews)
            .build()
    }
    
    // 3 стадия - Заказ в пути
    private fun onOrderOnTheWayNotification(minutesToDelivery: Int): Notification {
        val minuteString = if (minutesToDelivery > 1) "minutes" else "minute"

        return Notification.Builder(context, channelWithHighPriority)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentIntent(pendingIntent)
            .setOngoing(true)
            .setContentTitle("Live Notification - Your order is on its way and will be delivered in $minutesToDelivery $minuteString")
            .setCustomBigContentView(remoteViews)
            .build()
    }
    
    // 4 стадия - Заказ доставлен
    private fun onFinishNotification(): Notification {
        return Notification.Builder(context, channelWithHighPriority)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("Live Notification - Your order is delivered")
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .setCustomBigContentView(remoteViews) 
            .build()
    }

    // Функция для отображения уведомления первой стадии
    fun showNotification(stage: Int, stagesCount: Int) {
        val notification = onFirstNotification()
        remoteViews.setTextViewText(R.id.order_status, "Your order has been processed and will be collected soon")
        remoteViews.setTextViewText(R.id.stage_status, "Stage status $stage/$stagesCount")
        notificationManager.notify(notificationId, notification)
    }

    // Функция для обновления и отображения уведомления второй и третьей стадий
    fun updateNotification(minutesToDelivery: Int, stage: Int, stagesCount: Int) {
        val minuteString = if (minutesToDelivery > 1) "minutes" else "minute"
        
        when (stage) {
            2 -> {
                remoteViews.setTextViewText(R.id.order_status, "Your order is being assembled and will be shipped to you soon")
                remoteViews.setImageViewResource(R.id.image_stage, R.drawable.stage2)
            }
            3 -> {
                remoteViews.setTextViewText(R.id.order_status, "Your order is on its way and will be delivered in $minutesToDelivery $minuteString")
                remoteViews.setImageViewResource(R.id.image_stage, R.drawable.stage3)
            }
        }
        remoteViews.setTextViewText(R.id.stage_status, "Stage status $stage/$stagesCount")

        val notification: Notification? = when (stage) {
            2 -> {
                onGoingNotification()
            }
            3 -> {
                onOrderOnTheWayNotification(minutesToDelivery)
            }
            else -> null
        }
        
        if (notification != null) {
            notificationManager.notify(notificationId, notification)
        } else {
            println("Error: Notification is null.")
        }

        notificationManager.notify(notificationId, notification)
    }

    // Функция для отображения уведомления четвертой стадии
    fun finishDeliveryNotification(stage: Int, stagesCount: Int) {
        val notification = onFinishNotification()
        remoteViews.setTextViewText(R.id.order_status, "Your order is delivered. Enjoy your purchase!")
        remoteViews.setImageViewResource(R.id.image_stage, R.drawable.stage4)
        remoteViews.setTextViewText(R.id.stage_status, "Stage status $stage/$stagesCount")
        notificationManager.notify(notificationId, notification)
    }

    // Функция для удаления каналов при окончании жизненного цикла уведомления (просто скрыли)
    fun endNotification() {
        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.deleteNotificationChannel(channelWithHighPriority)
        notificationManager.deleteNotificationChannel(channelWithDefaultPriority)
        remoteViews.setViewVisibility(R.id.order_status, View.GONE)
    }
}

Если у вас глаза стали по 5 копеек, не переживайте — сейчас разберем основные моменты в этом, пусть и относительно повторяющемся, но важном коде.

Ключевой момент — это переме нная remoteViews, о которой говорится все это время. Этот объект связывает разметку, которая будет отображаться в уведомлениях, с текущими данными, такими как статус заказа, изображение и текст. В данном случае RemoteViews инициализируется через имя пакета и разметку, которую мы заранее создали в XML, и позволяет динамически обновлять содержимое уведомления.

// 1 стадия -  Заказ оформлен
private fun onFirstNotification(): Notification {
    // Создаем новый билд уведомления с указанием канала с высоким приоритетом
    return Notification.Builder(context, channelWithHighPriority)
        // Устанавливаем маленькую иконку для уведомления
        .setSmallIcon(R.drawable.notification_icon)
        // Устанавливаем заголовок уведомления
        .setContentTitle("Live Notification - Your order has been processed")
        // Устанавливаем действие при нажатии на уведомление - открывается MainActivity
        .setContentIntent(pendingIntent)
        // Устанавливаем флаг, чтобы уведомление оставалось на экране
        .setOngoing(true)
        // Используем кастомную разметку для уведомления, связанную с remoteViews
        .setCustomBigContentView(remoteViews)
        // Строим и возвращаем уведомление
        .build()
}

onFirstNotification() и подобные ей приватные функции отвечают за создание «скелета» уведомления для «Live Activity», которое показывает статус заказа. Мы настраиваем его с помощью Notification.Builder, добавляя иконку, заголовок и действие при нажатии. Чтобы уведомление не пропадало, используем setOngoing(true), а привязка через setCustomBigContentViewпозволит нам менять содержимое этого уведомления в будущем.

  // Функция для отображения уведомления первой стадии
    fun showNotification(stage: Int, stagesCount: Int)  {
        val notification = onFirstNotification()
        
        remoteViews.setTextViewText(R.id.order_status, "Your order has been processed and will be collected soon")
        /* remoteViews.setImageViewResource(R.id.image_stage, R.drawable.stage2)
        Для установки изображений мы бы использовали такую конструкцию, но поскольку
        у нас картинка по умолчанию - это картинка 1 этапа(стадии), то меня все устраивает.
        */
        remoteViews.setTextViewText(R.id.stage_status, "Stage status $stage/$stagesCount")

        notificationManager.notify(notificationId, notification)
    }

А как раз такие публичные функции берутэтот «скелет» и наполняют его актуальными данными. Они обновляют текстовые поля в remoteViews, напирмер, отображая текущий статус заказа и этап выполнения. Далее передают готовое уведомление в notificationManager, чтобы оно появилось на экране или обновилось, если уже активно.

Единственный, кто отличается логикой в своем теле, это:

  fun endNotification() {
    // Получаем NotificationManager для работы с уведомлениями
    val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    
    // Удаляем каналы уведомлений с высоким и обычным приоритетом
    notificationManager.deleteNotificationChannel(channelWithHighPriority)
    notificationManager.deleteNotificationChannel(channelWithDefaultPriority)
    
    // Скрываем элементы UI, которые отображают статус заказа и статус этапа
    remoteViews.setViewVisibility(R.id.order_status, View.GONE)
    remoteViews.setViewVisibility(R.id.stage_status, View.GONE)
}

Как несложно догадаться, функция endNotification() предназначена для завершения работы с уведомлениями. Она удаляет каналы уведомлений и скрывает элементы UI, отображающий статус заказа и статус этапа, что-то вроде ручного завершения или очистки. Мы будем вызывать её, когда пользователь скрывает уведомления, чтобы избежать лишней нагрузки на систему и освободить ресурсы.

Самое сложное позади! Осталось только завершить нативную часть, дописав методы, которые мы только что создали, и связать их с логикой обработки уведомлений.

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
      super.configureFlutterEngine(flutterEngine)
      MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
        call, result ->
        if (call.method == "startDelivery") {
            val args = call.arguments<Map<String, Any>>()
            val stage = args?.get("stage") as? Int
            val stagesCount = args?.get("stagesCount") as? Int

            
            if (stage != null && stagesCount != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    LiveActivityManager(this@MainActivity).showNotification(stage, stagesCount)
                }
            }
            result.success("Notification displayed")
        } else if (call.method == "updateDeliveryStatus") {
            val args = call.arguments<Map<String, Any>>()
            val minutes = args?.get("minutesToDelivery") as? Int
            val stage = args?.get("stage") as? Int
            val stagesCount = args?.get("stagesCount") as? Int

            if (minutes != null && stage != null && stagesCount != null){
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    LiveActivityManager(this@MainActivity).updateNotification(minutes, stage, stagesCount)
                }
            }
            result.success("Notification updated")
        } else if (call.method == "finishDelivery") {
            val args = call.arguments<Map<String, Any>>()
            val stage = args?.get("stage") as? Int
            val stagesCount = args?.get("stagesCount") as? Int

            if (stage != null && stagesCount != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    LiveActivityManager(this@MainActivity)
                        .finishDeliveryNotification(stage, stagesCount)
                }
            }
            result.success("Notification delivered")
        } else if (call.method == "endNotifications") { 
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                LiveActivityManager(this@MainActivity)
                    .endNotification()
            }
            result.success("Notification cancelled")
        }
      }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            ActivityCompat.requestPermissions(this, permissions, 200)
        }
    }

    override fun onStop() {
        super.onStop()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            LiveActivityManager(context).endNotification()
        }
    }

Основное, что стоит отметить, — это то, как мы связали Flutter и нативный код Android через MethodChannel. С помощью этого канала можно передавать данные из Flutter в Android и обратно. Например, при вызове startDelivery, updateDeliveryStatus или finishDelivery мы получаем данные, такие как минуты или общее количество стадий доставки, и на их основе запускаем или обновляем уведомления через LiveActivityManager.

Проверки версии SDK не менее важны, чем всё остальное. Например, перед использованием функций уведомлений, доступных только в определённых версиях Android, необходимо убедиться, что устройство поддерживает их. Так, если версия SDK соответствует Android 8.0 (Oreo) или выше (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O), можно безопасно создавать каналы уведомлений. Кроме того, начиная с Android 13 (Tiramisu), в системе появились дополнительные ограничения и изменения в поведении уведомлений, которые также стоит учитывать при разработке.

Стоит также обратить внимание на val args = call.arguments<Map<String, Any>>(). Это ключевой элемент, который позволяет принимать данные, передаваемые из Flutter в JSON формате. В нашем случае это информация о стадии доставки, количестве минут и других параметрах, необходимых для отправки уведомлений.

Этап 5. Радуемся и смотрим на результат

Теперь осталось завершить работу во Flutter и посмотреть, что у нас получилось.

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  LiveActivityAndroidService liveActivityService = LiveActivityAndroidService();

  LiveActivityModel liveActivityModel =
      LiveActivityModel(stage: 1, minutesToDelivery: 10, stagesCount: 4);

  Timer? timer;

  @override
  void dispose() {
    endNotifications();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: const Text("Live activity in Android"),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Center(
            child: ElevatedButton(
              onPressed: () async {
                setState(() {
                  liveActivityModel = LiveActivityModel(
                      stage: 1, minutesToDelivery: 10, stagesCount: 4);
                });
                await liveActivityService
                    .startNotifications(data: liveActivityModel)
                    .then((value) {
                  startNotifications();
                });
              },
              child: const Text("Start Notifications"),
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () {
              endNotifications();
            },
            child: const Text("End Notifications"),
          )
        ],
      ),
    );
  }

  void startNotifications() {
    timer?.cancel();

    timer = Timer.periodic(const Duration(seconds: 10), (value) async {
      liveActivityModel.stage += 1;
      liveActivityModel.minutesToDelivery -= 3;

      if (liveActivityModel.stage == 2 || liveActivityModel.stage == 3) {
        await liveActivityService.updateNotifications(data: liveActivityModel);
      }

      if (liveActivityModel.stage == 4) {
        await liveActivityService
            .finishNotifications(data: liveActivityModel)
            .then((value) {
          timer?.cancel();
        });
      }
    });
  }

  void endNotifications() {
    timer?.cancel();
    setState(() {
      liveActivityModel =
          LiveActivityModel(stage: 1, minutesToDelivery: 10, stagesCount: 4);
    });
    liveActivityService.endNotifications(data: liveActivityModel);
  }
}

Напишем UI, который содержит две кнопки: одна отвечает за запуск процесса отправки уведомлений, другая — за их остановку и сброс данных. При запуске создаётся экземпляр LiveActivityModel, который хранит информацию о текущем этапе доставки, оставшемся времени и общем количестве этапов. После нажатия кнопки старта создаётся таймер, который каждые 10 секунд обновляет уведомление, изменяя данные о стадии доставки и времени до завершения. Когда процесс достигает финального этапа, уведомления автоматически завершаются. При остановке процесса все данные сбрасываются к начальному состоянию. В целом этого для эмуляции данных и отображения нашего «Live Activity» вполне достаточно)

3. Итог

Хотя в Android нет точного аналога Live Activity, то, что она предоставляет, уже достаточно хорошо и может покрывать задачи, для которых она была создана. Если у вас есть вопросы или предложения по улучшению — пишите в комментариях! А так же хочется сказать, что английский вариант статьи и сорсы есть у меня на сайте. А мои подписчики в тг всегда в курсе всех моих проделок и узнают все самыми первыми! Подписывайтесь, скоро будет еще одна не менее интересная статья! ;D

Комментарии (0)