2024-01-10 18:54:00 +01:00
|
|
|
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel Dakhno,
|
|
|
|
Daniele Gobbetti, Davis Mosenkovs, Dmitriy Bogdanov, Felix Konstantin Maurer,
|
|
|
|
Ganblejs, José Rebelo, Pauli Salmenrinne, Petr Vaněk, Roberto P. Rubio,
|
|
|
|
Taavi Eomäe, Uwe Hermann, Yar
|
2017-03-10 14:53:19 +01:00
|
|
|
|
|
|
|
This file is part of Gadgetbridge.
|
|
|
|
|
|
|
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Gadgetbridge is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
2024-01-10 18:54:00 +01:00
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
2015-08-03 23:09:49 +02:00
|
|
|
package nodomain.freeyourgadget.gadgetbridge.util;
|
2015-04-13 01:01:52 +02:00
|
|
|
|
2024-02-13 10:54:12 +01:00
|
|
|
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.isRunningOreoOrLater;
|
|
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_RECORDED_DATA_TYPES;
|
|
|
|
|
2015-07-11 21:49:51 +02:00
|
|
|
import android.app.Activity;
|
2015-04-13 01:01:52 +02:00
|
|
|
import android.app.Notification;
|
2018-06-19 22:03:49 +02:00
|
|
|
import android.app.NotificationChannel;
|
|
|
|
import android.app.NotificationManager;
|
2015-04-13 01:01:52 +02:00
|
|
|
import android.app.PendingIntent;
|
2015-05-07 22:15:53 +02:00
|
|
|
import android.bluetooth.BluetoothAdapter;
|
2015-04-13 01:01:52 +02:00
|
|
|
import android.content.Context;
|
|
|
|
import android.content.Intent;
|
|
|
|
import android.content.pm.PackageManager;
|
2015-07-11 21:49:51 +02:00
|
|
|
import android.os.Handler;
|
|
|
|
import android.os.Looper;
|
2022-06-14 18:05:41 +02:00
|
|
|
import android.text.Html;
|
|
|
|
import android.text.SpannableString;
|
2015-07-11 21:49:51 +02:00
|
|
|
import android.widget.Toast;
|
2015-05-12 06:28:11 +02:00
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
import androidx.annotation.NonNull;
|
2022-08-27 15:12:47 +02:00
|
|
|
import androidx.core.app.ActivityCompat;
|
2020-08-02 10:55:06 +02:00
|
|
|
import androidx.core.app.NotificationCompat;
|
|
|
|
import androidx.core.app.NotificationManagerCompat;
|
|
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
|
|
|
2015-05-12 06:28:11 +02:00
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
2015-05-01 01:49:43 +02:00
|
|
|
|
2015-06-25 23:34:50 +02:00
|
|
|
import java.io.File;
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
import java.io.IOException;
|
2024-01-04 17:48:26 +01:00
|
|
|
import java.util.Collections;
|
2022-06-14 18:05:41 +02:00
|
|
|
import java.util.List;
|
2015-06-25 23:34:50 +02:00
|
|
|
|
2015-08-03 23:09:49 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
2015-08-22 01:08:46 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
|
2015-08-03 23:09:49 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
2016-10-25 17:49:21 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
2018-01-07 12:50:59 +01:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
2015-06-24 23:55:51 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
2017-10-20 22:49:53 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
2020-04-20 00:11:45 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
2017-10-20 22:49:53 +02:00
|
|
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
|
2018-06-19 22:03:49 +02:00
|
|
|
|
2015-04-13 01:01:52 +02:00
|
|
|
public class GB {
|
2018-02-14 21:27:24 +01:00
|
|
|
|
|
|
|
public static final String NOTIFICATION_CHANNEL_ID = "gadgetbridge";
|
2023-12-26 05:46:24 +01:00
|
|
|
public static final String NOTIFICATION_CHANNEL_ID_CONNECTION_STATUS = "gadgetbridge connection status";
|
2024-02-26 00:15:34 +01:00
|
|
|
public static final String NOTIFICATION_CHANNEL_ID_SCAN_SERVICE = "gadgetbridge_scan_service";
|
2020-03-03 18:01:39 +01:00
|
|
|
public static final String NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID = "gadgetbridge_high_priority";
|
2018-06-19 22:03:49 +02:00
|
|
|
public static final String NOTIFICATION_CHANNEL_ID_TRANSFER = "gadgetbridge transfer";
|
2021-05-14 19:03:22 +02:00
|
|
|
public static final String NOTIFICATION_CHANNEL_ID_LOW_BATTERY = "low_battery";
|
2022-06-04 22:20:28 +02:00
|
|
|
public static final String NOTIFICATION_CHANNEL_ID_GPS = "gps";
|
2018-02-14 21:27:24 +01:00
|
|
|
|
2015-04-13 01:01:52 +02:00
|
|
|
public static final int NOTIFICATION_ID = 1;
|
2015-07-28 23:10:21 +02:00
|
|
|
public static final int NOTIFICATION_ID_INSTALL = 2;
|
2015-08-18 17:37:51 +02:00
|
|
|
public static final int NOTIFICATION_ID_LOW_BATTERY = 3;
|
2015-09-24 14:03:01 +02:00
|
|
|
public static final int NOTIFICATION_ID_TRANSFER = 4;
|
2018-01-07 12:50:59 +01:00
|
|
|
public static final int NOTIFICATION_ID_EXPORT_FAILED = 5;
|
2020-03-03 18:01:39 +01:00
|
|
|
public static final int NOTIFICATION_ID_PHONE_FIND = 6;
|
2022-06-04 22:20:28 +02:00
|
|
|
public static final int NOTIFICATION_ID_GPS = 7;
|
2024-02-14 03:25:26 +01:00
|
|
|
public static final int NOTIFICATION_ID_SCAN = 8;
|
2021-05-14 18:30:54 +02:00
|
|
|
public static final int NOTIFICATION_ID_ERROR = 42;
|
2015-07-28 23:10:21 +02:00
|
|
|
|
2015-05-12 06:28:11 +02:00
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(GB.class);
|
2015-07-11 21:49:51 +02:00
|
|
|
public static final int INFO = 1;
|
|
|
|
public static final int WARN = 2;
|
|
|
|
public static final int ERROR = 3;
|
2016-04-03 00:50:45 +02:00
|
|
|
public static final String ACTION_DISPLAY_MESSAGE = "GB_Display_Message";
|
|
|
|
public static final String DISPLAY_MESSAGE_MESSAGE = "message";
|
|
|
|
public static final String DISPLAY_MESSAGE_DURATION = "duration";
|
|
|
|
public static final String DISPLAY_MESSAGE_SEVERITY = "severity";
|
2015-04-13 01:01:52 +02:00
|
|
|
|
2020-10-04 01:11:40 +02:00
|
|
|
/** Commands related to the progress (bar) on the screen */
|
|
|
|
public static final String ACTION_SET_PROGRESS_BAR = "GB_Set_Progress_Bar";
|
|
|
|
public static final String PROGRESS_BAR_INDETERMINATE = "indeterminate";
|
|
|
|
public static final String PROGRESS_BAR_MAX = "max";
|
|
|
|
public static final String PROGRESS_BAR_PROGRESS = "progress";
|
|
|
|
public static final String ACTION_SET_PROGRESS_TEXT = "GB_Set_Progress_Text";
|
|
|
|
public static final String ACTION_SET_INFO_TEXT = "GB_Set_Info_Text";
|
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
private static boolean notificationChannelsCreated;
|
|
|
|
|
2022-08-05 20:26:54 +02:00
|
|
|
private static final String TAG = "GB";
|
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
public static void createNotificationChannels(Context context) {
|
|
|
|
if (notificationChannelsCreated) return;
|
|
|
|
|
|
|
|
if (isRunningOreoOrLater()) {
|
|
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
|
|
|
|
|
|
NotificationChannel channelGeneral = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID,
|
|
|
|
context.getString(R.string.notification_channel_name),
|
|
|
|
NotificationManager.IMPORTANCE_LOW);
|
|
|
|
notificationManager.createNotificationChannel(channelGeneral);
|
|
|
|
|
2023-12-26 05:46:24 +01:00
|
|
|
NotificationChannel channelConnwectionStatus = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID_CONNECTION_STATUS,
|
|
|
|
context.getString(R.string.notification_channel_connection_status_name),
|
|
|
|
NotificationManager.IMPORTANCE_LOW);
|
|
|
|
notificationManager.createNotificationChannel(channelConnwectionStatus);
|
|
|
|
|
2024-02-26 00:15:34 +01:00
|
|
|
NotificationChannel channelScanService = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID_SCAN_SERVICE,
|
|
|
|
context.getString(R.string.notification_channel_scan_service_name),
|
|
|
|
NotificationManager.IMPORTANCE_LOW);
|
|
|
|
notificationManager.createNotificationChannel(channelScanService);
|
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
NotificationChannel channelHighPriority = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID,
|
|
|
|
context.getString(R.string.notification_channel_high_priority_name),
|
|
|
|
NotificationManager.IMPORTANCE_HIGH);
|
|
|
|
notificationManager.createNotificationChannel(channelHighPriority);
|
|
|
|
|
|
|
|
NotificationChannel channelTransfer = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID_TRANSFER,
|
2021-05-14 18:49:00 +02:00
|
|
|
context.getString(R.string.notification_channel_transfer_name),
|
2021-05-14 18:30:54 +02:00
|
|
|
NotificationManager.IMPORTANCE_LOW);
|
|
|
|
notificationManager.createNotificationChannel(channelTransfer);
|
2021-05-14 19:03:22 +02:00
|
|
|
|
|
|
|
NotificationChannel channelLowBattery = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID_LOW_BATTERY,
|
|
|
|
context.getString(R.string.notification_channel_low_battery_name),
|
|
|
|
NotificationManager.IMPORTANCE_DEFAULT);
|
|
|
|
notificationManager.createNotificationChannel(channelLowBattery);
|
2022-06-04 22:20:28 +02:00
|
|
|
|
|
|
|
NotificationChannel channelGps = new NotificationChannel(
|
|
|
|
NOTIFICATION_CHANNEL_ID_GPS,
|
|
|
|
context.getString(R.string.notification_channel_gps),
|
|
|
|
NotificationManager.IMPORTANCE_MIN);
|
|
|
|
notificationManager.createNotificationChannel(channelGps);
|
2021-05-14 18:30:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
notificationChannelsCreated = true;
|
|
|
|
}
|
|
|
|
|
2017-10-20 22:49:53 +02:00
|
|
|
private static PendingIntent getContentIntent(Context context) {
|
2016-10-25 17:49:21 +02:00
|
|
|
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
2015-04-13 01:01:52 +02:00
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
|
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0,
|
|
|
|
notificationIntent, 0, false);
|
2015-04-13 01:01:52 +02:00
|
|
|
|
2017-10-20 22:49:53 +02:00
|
|
|
return pendingIntent;
|
|
|
|
}
|
|
|
|
|
2022-06-14 18:05:41 +02:00
|
|
|
public static Notification createNotification(List<GBDevice> devices, Context context) {
|
2023-12-26 05:46:24 +01:00
|
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_CONNECTION_STATUS);
|
2022-06-14 18:05:41 +02:00
|
|
|
if(devices.size() == 0){
|
|
|
|
builder.setContentTitle(context.getString(R.string.info_no_devices_connected))
|
|
|
|
.setSmallIcon(R.drawable.ic_notification_disconnected)
|
|
|
|
.setContentIntent(getContentIntent(context))
|
|
|
|
.setShowWhen(false)
|
|
|
|
.setOngoing(true);
|
|
|
|
|
|
|
|
if (!GBApplication.isRunningTwelveOrLater()) {
|
|
|
|
builder.setColor(context.getResources().getColor(R.color.accent));
|
|
|
|
}
|
|
|
|
}else if(devices.size() == 1) {
|
|
|
|
GBDevice device = devices.get(0);
|
|
|
|
String deviceName = device.getAliasOrName();
|
|
|
|
String text = device.getStateString();
|
|
|
|
if (device.getBatteryLevel() != GBDevice.BATTERY_UNKNOWN) {
|
|
|
|
text += ": " + context.getString(R.string.battery) + " " + device.getBatteryLevel() + "%";
|
|
|
|
}
|
2017-10-20 22:49:53 +02:00
|
|
|
|
2022-06-14 18:05:41 +02:00
|
|
|
boolean connected = device.isInitialized();
|
|
|
|
builder.setContentTitle(deviceName)
|
|
|
|
.setTicker(deviceName + " - " + text)
|
|
|
|
.setContentText(text)
|
|
|
|
.setSmallIcon(connected ? device.getNotificationIconConnected() : device.getNotificationIconDisconnected())
|
|
|
|
.setContentIntent(getContentIntent(context))
|
|
|
|
.setShowWhen(false)
|
|
|
|
.setOngoing(true);
|
|
|
|
|
|
|
|
if (!GBApplication.isRunningTwelveOrLater()) {
|
|
|
|
builder.setColor(context.getResources().getColor(R.color.accent));
|
|
|
|
}
|
2022-04-30 21:24:34 +02:00
|
|
|
|
2022-06-14 18:05:41 +02:00
|
|
|
Intent deviceCommunicationServiceIntent = new Intent(context, DeviceCommunicationService.class);
|
|
|
|
if (connected) {
|
|
|
|
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_DISCONNECT);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent disconnectPendingIntent = PendingIntentUtils.getService(context, 0, deviceCommunicationServiceIntent, PendingIntent.FLAG_ONE_SHOT, false);
|
2022-06-14 18:05:41 +02:00
|
|
|
builder.addAction(R.drawable.ic_notification_disconnected, context.getString(R.string.controlcenter_disconnect), disconnectPendingIntent);
|
2023-09-27 23:11:02 +02:00
|
|
|
if (device.getDeviceCoordinator().supportsActivityDataFetching()) {
|
2022-06-14 18:05:41 +02:00
|
|
|
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA);
|
|
|
|
deviceCommunicationServiceIntent.putExtra(EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent fetchPendingIntent = PendingIntentUtils.getService(context, 1, deviceCommunicationServiceIntent, PendingIntent.FLAG_ONE_SHOT, false);
|
2022-06-14 18:05:41 +02:00
|
|
|
builder.addAction(R.drawable.ic_refresh, context.getString(R.string.controlcenter_fetch_activity_data), fetchPendingIntent);
|
|
|
|
}
|
|
|
|
} else if (device.getState().equals(GBDevice.State.WAITING_FOR_RECONNECT) || device.getState().equals(GBDevice.State.NOT_CONNECTED)) {
|
|
|
|
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_CONNECT);
|
|
|
|
deviceCommunicationServiceIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent reconnectPendingIntent = PendingIntentUtils.getService(context, 2, deviceCommunicationServiceIntent, PendingIntent.FLAG_UPDATE_CURRENT, false);
|
2022-06-14 18:05:41 +02:00
|
|
|
builder.addAction(R.drawable.ic_notification, context.getString(R.string.controlcenter_connect), reconnectPendingIntent);
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
StringBuilder contentText = new StringBuilder();
|
|
|
|
boolean isConnected = true;
|
|
|
|
boolean anyDeviceSupportesActivityDataFetching = false;
|
|
|
|
for(GBDevice device : devices){
|
|
|
|
if(!device.isInitialized()){
|
|
|
|
isConnected = false;
|
|
|
|
}
|
|
|
|
|
2023-09-27 23:11:02 +02:00
|
|
|
anyDeviceSupportesActivityDataFetching |= device.getDeviceCoordinator().supportsActivityDataFetching();
|
2022-06-14 18:05:41 +02:00
|
|
|
|
|
|
|
String deviceName = device.getAliasOrName();
|
|
|
|
String text = device.getStateString();
|
|
|
|
if (device.getBatteryLevel() != GBDevice.BATTERY_UNKNOWN) {
|
|
|
|
text += ": " + context.getString(R.string.battery) + " " + device.getBatteryLevel() + "%";
|
|
|
|
}
|
|
|
|
contentText.append(deviceName).append(" (").append(text).append(")<br>");
|
|
|
|
}
|
|
|
|
|
|
|
|
SpannableString formated = new SpannableString(
|
|
|
|
Html.fromHtml(contentText.substring(0, contentText.length() - 4)) // cut away last <br>
|
|
|
|
);
|
|
|
|
|
|
|
|
String title = context.getString(R.string.info_connected_count, devices.size());
|
|
|
|
|
|
|
|
builder.setContentTitle(title)
|
|
|
|
.setContentText(formated)
|
|
|
|
.setSmallIcon(isConnected ? R.drawable.ic_notification : R.drawable.ic_notification_disconnected)
|
|
|
|
.setContentIntent(getContentIntent(context))
|
|
|
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(formated).setBigContentTitle(title))
|
|
|
|
.setShowWhen(false)
|
|
|
|
.setOngoing(true);
|
|
|
|
|
|
|
|
if (!GBApplication.isRunningTwelveOrLater()) {
|
|
|
|
builder.setColor(context.getResources().getColor(R.color.accent));
|
|
|
|
}
|
|
|
|
|
2022-09-09 19:58:34 +02:00
|
|
|
if (anyDeviceSupportesActivityDataFetching) {
|
2022-06-14 18:05:41 +02:00
|
|
|
Intent deviceCommunicationServiceIntent = new Intent(context, DeviceCommunicationService.class);
|
2018-03-31 16:21:25 +02:00
|
|
|
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_FETCH_RECORDED_DATA);
|
2020-04-20 00:11:45 +02:00
|
|
|
deviceCommunicationServiceIntent.putExtra(EXTRA_RECORDED_DATA_TYPES, ActivityKind.TYPE_ACTIVITY);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent fetchPendingIntent = PendingIntentUtils.getService(context, 1, deviceCommunicationServiceIntent, PendingIntent.FLAG_ONE_SHOT, false);
|
2020-09-01 21:27:07 +02:00
|
|
|
builder.addAction(R.drawable.ic_refresh, context.getString(R.string.controlcenter_fetch_activity_data), fetchPendingIntent);
|
2017-10-20 22:49:53 +02:00
|
|
|
}
|
|
|
|
}
|
2022-06-14 18:05:41 +02:00
|
|
|
|
2022-09-09 19:58:34 +02:00
|
|
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
|
|
|
|
2017-10-20 22:49:53 +02:00
|
|
|
if (GBApplication.minimizeNotification()) {
|
|
|
|
builder.setPriority(Notification.PRIORITY_MIN);
|
|
|
|
}
|
|
|
|
return builder.build();
|
|
|
|
}
|
|
|
|
|
2017-10-30 21:37:31 +01:00
|
|
|
public static Notification createNotification(String text, Context context) {
|
2023-12-26 05:46:24 +01:00
|
|
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_CONNECTION_STATUS);
|
2017-10-30 21:37:31 +01:00
|
|
|
builder.setTicker(text)
|
2015-04-13 01:01:52 +02:00
|
|
|
.setContentText(text)
|
2017-10-30 21:37:31 +01:00
|
|
|
.setSmallIcon(R.drawable.ic_notification_disconnected)
|
2017-10-20 22:49:53 +02:00
|
|
|
.setContentIntent(getContentIntent(context))
|
2021-05-14 20:01:32 +02:00
|
|
|
.setShowWhen(false)
|
2015-12-07 23:08:24 +01:00
|
|
|
.setOngoing(true);
|
2022-04-30 21:24:34 +02:00
|
|
|
|
|
|
|
if (!GBApplication.isRunningTwelveOrLater()) {
|
|
|
|
builder.setColor(context.getResources().getColor(R.color.accent));
|
|
|
|
}
|
|
|
|
|
2024-01-04 17:48:26 +01:00
|
|
|
// A small bug: When "Reconnect only to connected devices" is disabled, the intent will be added even when there are no devices in GB
|
|
|
|
// Not sure whether it is worth the complexity to fix this
|
|
|
|
if (!GBApplication.getPrefs().getBoolean(GBPrefs.RECONNECT_ONLY_TO_CONNECTED, true) || !GBApplication.getPrefs().getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet()).isEmpty()) {
|
2017-10-30 21:37:31 +01:00
|
|
|
Intent deviceCommunicationServiceIntent = new Intent(context, DeviceCommunicationService.class);
|
|
|
|
deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_CONNECT);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent reconnectPendingIntent = PendingIntentUtils.getService(context, 2, deviceCommunicationServiceIntent, PendingIntent.FLAG_ONE_SHOT, false);
|
2017-10-30 21:37:31 +01:00
|
|
|
builder.addAction(R.drawable.ic_notification, context.getString(R.string.controlcenter_connect), reconnectPendingIntent);
|
|
|
|
}
|
2022-09-09 19:58:34 +02:00
|
|
|
|
|
|
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
|
|
|
|
2017-01-08 15:51:56 +01:00
|
|
|
if (GBApplication.minimizeNotification()) {
|
|
|
|
builder.setPriority(Notification.PRIORITY_MIN);
|
|
|
|
}
|
2015-12-07 23:08:24 +01:00
|
|
|
return builder.build();
|
2015-04-13 01:01:52 +02:00
|
|
|
}
|
|
|
|
|
2022-06-14 18:05:41 +02:00
|
|
|
public static void updateNotification(List<GBDevice> devices, Context context) {
|
|
|
|
Notification notification = createNotification(devices, context);
|
2021-05-14 18:30:54 +02:00
|
|
|
notify(NOTIFICATION_ID, notification, context);
|
2015-08-29 20:38:53 +02:00
|
|
|
}
|
2015-04-13 01:01:52 +02:00
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
public static void notify(int id, @NonNull Notification notification, Context context) {
|
|
|
|
createNotificationChannels(context);
|
|
|
|
|
2024-02-13 10:54:12 +01:00
|
|
|
try {
|
|
|
|
NotificationManagerCompat.from(context).notify(id, notification);
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
toast(context.getString(R.string.warning_missing_notification_permission), Toast.LENGTH_SHORT, WARN);
|
|
|
|
}
|
2015-04-13 01:01:52 +02:00
|
|
|
}
|
|
|
|
|
2021-05-14 18:30:54 +02:00
|
|
|
public static void removeNotification(int id, Context context) {
|
|
|
|
NotificationManagerCompat.from(context).cancel(id);
|
2015-09-24 14:03:01 +02:00
|
|
|
}
|
|
|
|
|
2015-08-03 23:09:49 +02:00
|
|
|
public static boolean isBluetoothEnabled() {
|
2015-05-07 22:15:53 +02:00
|
|
|
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
|
|
|
return adapter != null && adapter.isEnabled();
|
|
|
|
}
|
|
|
|
|
2015-05-09 23:54:47 +02:00
|
|
|
public static boolean supportsBluetoothLE() {
|
|
|
|
return GBApplication.getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
|
|
|
|
}
|
|
|
|
|
2020-10-02 01:02:58 +02:00
|
|
|
public static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
|
|
|
|
|
2015-04-26 00:53:48 +02:00
|
|
|
public static String hexdump(byte[] buffer, int offset, int length) {
|
2015-06-24 23:55:51 +02:00
|
|
|
if (length == -1) {
|
2015-10-04 15:42:21 +02:00
|
|
|
length = buffer.length - offset;
|
2015-06-16 23:14:51 +02:00
|
|
|
}
|
2020-10-02 01:02:58 +02:00
|
|
|
|
2015-04-25 23:13:22 +02:00
|
|
|
char[] hexChars = new char[length * 2];
|
2015-04-26 00:53:48 +02:00
|
|
|
for (int i = 0; i < length; i++) {
|
|
|
|
int v = buffer[i + offset] & 0xFF;
|
2020-10-02 01:02:58 +02:00
|
|
|
hexChars[i * 2] = HEX_CHARS[v >>> 4];
|
|
|
|
hexChars[i * 2 + 1] = HEX_CHARS[v & 0x0F];
|
2015-04-25 23:13:22 +02:00
|
|
|
}
|
|
|
|
return new String(hexChars);
|
|
|
|
}
|
2015-05-05 00:48:02 +02:00
|
|
|
|
2020-10-02 01:02:58 +02:00
|
|
|
public static String hexdump(byte[] buffer) {
|
|
|
|
return hexdump(buffer, 0, buffer.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* https://stackoverflow.com/a/140861/4636860
|
|
|
|
*/
|
2019-07-23 08:56:26 +02:00
|
|
|
public static byte[] hexStringToByteArray(String s) {
|
|
|
|
int len = s.length();
|
|
|
|
byte[] data = new byte[len / 2];
|
|
|
|
for (int i = 0; i < len; i += 2) {
|
|
|
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
2020-08-02 10:55:06 +02:00
|
|
|
+ Character.digit(s.charAt(i + 1), 16));
|
2019-07-23 08:56:26 +02:00
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2015-05-05 00:48:02 +02:00
|
|
|
public static String formatRssi(short rssi) {
|
|
|
|
return String.valueOf(rssi);
|
|
|
|
}
|
2015-06-24 23:55:51 +02:00
|
|
|
|
2015-10-28 23:54:08 +01:00
|
|
|
public static String writeScreenshot(GBDeviceEventScreenshot screenshot, String filename) throws IOException {
|
2023-06-11 17:54:50 +02:00
|
|
|
LOG.info("Will write screenshot as {}", filename);
|
2015-06-25 23:34:50 +02:00
|
|
|
|
2023-06-11 17:54:50 +02:00
|
|
|
final File dir = FileUtils.getExternalFilesDir();
|
|
|
|
final File outputFile = new File(dir, filename);
|
2015-10-28 23:54:08 +01:00
|
|
|
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
2023-06-11 17:54:50 +02:00
|
|
|
fos.write(screenshot.getData());
|
2015-06-25 23:34:50 +02:00
|
|
|
}
|
2015-10-28 23:54:08 +01:00
|
|
|
return outputFile.getAbsolutePath();
|
2015-06-24 23:55:51 +02:00
|
|
|
}
|
2015-07-11 21:49:51 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates and display a Toast message using the application context.
|
|
|
|
* Additionally the toast is logged using the provided severity.
|
|
|
|
* Can be called from any thread.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
|
|
|
* @param message the message to display.
|
2015-07-11 21:49:51 +02:00
|
|
|
* @param displayTime something like Toast.LENGTH_SHORT
|
2015-07-25 21:52:52 +02:00
|
|
|
* @param severity either INFO, WARNING, ERROR
|
2015-07-11 21:49:51 +02:00
|
|
|
*/
|
|
|
|
public static void toast(String message, int displayTime, int severity) {
|
|
|
|
toast(GBApplication.getContext(), message, displayTime, severity, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates and display a Toast message using the application context.
|
|
|
|
* Additionally the toast is logged using the provided severity.
|
|
|
|
* Can be called from any thread.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
|
|
|
* @param message the message to display.
|
2015-07-11 21:49:51 +02:00
|
|
|
* @param displayTime something like Toast.LENGTH_SHORT
|
2015-07-25 21:52:52 +02:00
|
|
|
* @param severity either INFO, WARNING, ERROR
|
2015-07-11 21:49:51 +02:00
|
|
|
*/
|
|
|
|
public static void toast(String message, int displayTime, int severity, Throwable ex) {
|
|
|
|
toast(GBApplication.getContext(), message, displayTime, severity, ex);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates and display a Toast message using the application context
|
|
|
|
* Can be called from any thread.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
|
|
|
* @param context the context to use
|
|
|
|
* @param message the message to display
|
2015-07-11 21:49:51 +02:00
|
|
|
* @param displayTime something like Toast.LENGTH_SHORT
|
2015-07-25 21:52:52 +02:00
|
|
|
* @param severity either INFO, WARNING, ERROR
|
2015-07-11 21:49:51 +02:00
|
|
|
*/
|
|
|
|
public static void toast(final Context context, final String message, final int displayTime, final int severity) {
|
2015-07-25 21:52:52 +02:00
|
|
|
toast(context, message, displayTime, severity, null);
|
2015-07-11 21:49:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates and display a Toast message using the application context
|
|
|
|
* Can be called from any thread.
|
2015-07-25 21:52:52 +02:00
|
|
|
*
|
|
|
|
* @param context the context to use
|
|
|
|
* @param message the message to display
|
2015-07-11 21:49:51 +02:00
|
|
|
* @param displayTime something like Toast.LENGTH_SHORT
|
2015-07-25 21:52:52 +02:00
|
|
|
* @param severity either INFO, WARNING, ERROR
|
|
|
|
* @param ex optional exception to be logged
|
2015-07-11 21:49:51 +02:00
|
|
|
*/
|
|
|
|
public static void toast(final Context context, final String message, final int displayTime, final int severity, final Throwable ex) {
|
2016-02-24 23:53:30 +01:00
|
|
|
log(message, severity, ex); // log immediately, not delayed
|
2017-04-25 21:51:53 +02:00
|
|
|
if (GBEnvironment.env().isLocalTest()) {
|
2015-08-22 01:08:46 +02:00
|
|
|
return;
|
|
|
|
}
|
2015-07-11 21:49:51 +02:00
|
|
|
Looper mainLooper = Looper.getMainLooper();
|
|
|
|
if (Thread.currentThread() == mainLooper.getThread()) {
|
|
|
|
Toast.makeText(context, message, displayTime).show();
|
|
|
|
} else {
|
|
|
|
Runnable runnable = new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
Toast.makeText(context, message, displayTime).show();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (context instanceof Activity) {
|
|
|
|
((Activity) context).runOnUiThread(runnable);
|
|
|
|
} else {
|
|
|
|
new Handler(mainLooper).post(runnable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-03 00:50:45 +02:00
|
|
|
public static void log(String message, int severity, Throwable ex) {
|
2015-07-11 21:49:51 +02:00
|
|
|
switch (severity) {
|
|
|
|
case INFO:
|
|
|
|
LOG.info(message, ex);
|
|
|
|
break;
|
|
|
|
case WARN:
|
|
|
|
LOG.warn(message, ex);
|
|
|
|
break;
|
|
|
|
case ERROR:
|
|
|
|
LOG.error(message, ex);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2015-07-20 23:11:16 +02:00
|
|
|
|
2018-06-19 22:03:49 +02:00
|
|
|
private static Notification createTransferNotification(String title, String text, boolean ongoing,
|
2015-09-24 14:03:01 +02:00
|
|
|
int percentage, Context context) {
|
2016-10-25 17:49:21 +02:00
|
|
|
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
2015-09-24 14:03:01 +02:00
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
|
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0,
|
|
|
|
notificationIntent, 0, false);
|
2015-09-24 14:03:01 +02:00
|
|
|
|
2018-06-19 22:03:49 +02:00
|
|
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_TRANSFER)
|
|
|
|
.setTicker((title == null) ? context.getString(R.string.app_name) : title)
|
2017-05-28 18:50:41 +02:00
|
|
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
2018-06-19 22:03:49 +02:00
|
|
|
.setContentTitle((title == null) ? context.getString(R.string.app_name) : title)
|
|
|
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
|
2015-09-24 14:03:01 +02:00
|
|
|
.setContentText(text)
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
.setOngoing(ongoing);
|
|
|
|
|
|
|
|
if (ongoing) {
|
|
|
|
nb.setProgress(100, percentage, percentage == 0);
|
|
|
|
nb.setSmallIcon(android.R.drawable.stat_sys_download);
|
|
|
|
} else {
|
|
|
|
nb.setProgress(0, 0, false);
|
|
|
|
nb.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
|
|
|
}
|
|
|
|
|
|
|
|
return nb.build();
|
|
|
|
}
|
|
|
|
|
2018-06-19 22:03:49 +02:00
|
|
|
public static void updateTransferNotification(String title, String text, boolean ongoing, int percentage, Context context) {
|
2015-09-25 00:53:40 +02:00
|
|
|
if (percentage == 100) {
|
2015-09-24 14:03:01 +02:00
|
|
|
removeNotification(NOTIFICATION_ID_TRANSFER, context);
|
|
|
|
} else {
|
2018-06-19 22:03:49 +02:00
|
|
|
Notification notification = createTransferNotification(title, text, ongoing, percentage, context);
|
2021-05-14 18:30:54 +02:00
|
|
|
notify(NOTIFICATION_ID_TRANSFER, notification, context);
|
2015-09-24 14:03:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-04 22:20:28 +02:00
|
|
|
public static void createGpsNotification(Context context, int numDevices) {
|
|
|
|
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
|
2022-06-04 22:20:28 +02:00
|
|
|
|
|
|
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_GPS)
|
|
|
|
.setTicker(context.getString(R.string.notification_gps_title))
|
|
|
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
|
|
.setContentTitle(context.getString(R.string.notification_gps_title))
|
|
|
|
.setContentText(context.getString(R.string.notification_gps_text, numDevices))
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
.setSmallIcon(R.drawable.ic_gps_location)
|
|
|
|
.setOngoing(true);
|
|
|
|
|
|
|
|
notify(NOTIFICATION_ID_GPS, nb.build(), context);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void removeGpsNotification(Context context) {
|
|
|
|
removeNotification(NOTIFICATION_ID_GPS, context);
|
|
|
|
}
|
|
|
|
|
2015-07-28 18:44:54 +02:00
|
|
|
private static Notification createInstallNotification(String text, boolean ongoing,
|
2015-07-28 23:10:21 +02:00
|
|
|
int percentage, Context context) {
|
2016-10-25 17:49:21 +02:00
|
|
|
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
2015-07-28 18:44:54 +02:00
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
|
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0,
|
|
|
|
notificationIntent, 0, false);
|
2015-07-28 18:44:54 +02:00
|
|
|
|
2018-02-14 21:27:24 +01:00
|
|
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
2015-07-28 18:44:54 +02:00
|
|
|
.setContentTitle(context.getString(R.string.app_name))
|
|
|
|
.setContentText(text)
|
|
|
|
.setTicker(text)
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
.setOngoing(ongoing);
|
|
|
|
|
|
|
|
if (ongoing) {
|
|
|
|
nb.setProgress(100, percentage, percentage == 0);
|
2015-07-28 23:10:21 +02:00
|
|
|
nb.setSmallIcon(android.R.drawable.stat_sys_upload);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
nb.setSmallIcon(android.R.drawable.stat_sys_upload_done);
|
2015-07-28 18:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nb.build();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void updateInstallNotification(String text, boolean ongoing, int percentage, Context context) {
|
|
|
|
Notification notification = createInstallNotification(text, ongoing, percentage, context);
|
2021-05-14 18:30:54 +02:00
|
|
|
notify(NOTIFICATION_ID_INSTALL, notification, context);
|
2015-07-28 18:44:54 +02:00
|
|
|
}
|
2015-08-18 17:37:51 +02:00
|
|
|
|
2015-08-19 17:36:53 +02:00
|
|
|
private static Notification createBatteryNotification(String text, String bigText, Context context) {
|
2016-10-25 17:49:21 +02:00
|
|
|
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
|
2015-08-18 17:37:51 +02:00
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
|
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0,
|
|
|
|
notificationIntent, 0, false);
|
2015-08-18 17:37:51 +02:00
|
|
|
|
2021-05-14 19:03:22 +02:00
|
|
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_LOW_BATTERY)
|
2015-09-24 14:45:21 +02:00
|
|
|
.setContentTitle(context.getString(R.string.notif_battery_low_title))
|
2015-08-19 17:36:53 +02:00
|
|
|
.setContentText(text)
|
2015-08-18 17:37:51 +02:00
|
|
|
.setContentIntent(pendingIntent)
|
2015-08-19 17:36:53 +02:00
|
|
|
.setSmallIcon(R.drawable.ic_notification_low_battery)
|
2015-08-29 20:38:53 +02:00
|
|
|
.setPriority(Notification.PRIORITY_HIGH)
|
2015-08-18 17:37:51 +02:00
|
|
|
.setOngoing(false);
|
|
|
|
|
2015-08-19 17:36:53 +02:00
|
|
|
if (bigText != null) {
|
|
|
|
nb.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText));
|
|
|
|
}
|
|
|
|
|
2015-08-18 17:37:51 +02:00
|
|
|
return nb.build();
|
|
|
|
}
|
|
|
|
|
2015-08-19 17:36:53 +02:00
|
|
|
public static void updateBatteryNotification(String text, String bigText, Context context) {
|
2017-04-25 21:51:53 +02:00
|
|
|
if (GBEnvironment.env().isLocalTest()) {
|
2015-08-29 20:38:53 +02:00
|
|
|
return;
|
|
|
|
}
|
2015-08-19 17:36:53 +02:00
|
|
|
Notification notification = createBatteryNotification(text, bigText, context);
|
2021-05-14 18:30:54 +02:00
|
|
|
notify(NOTIFICATION_ID_LOW_BATTERY, notification, context);
|
2015-08-18 17:37:51 +02:00
|
|
|
}
|
2015-08-24 17:48:17 +02:00
|
|
|
|
2017-04-24 09:53:48 +02:00
|
|
|
public static void removeBatteryNotification(Context context) {
|
|
|
|
removeNotification(NOTIFICATION_ID_LOW_BATTERY, context);
|
|
|
|
}
|
|
|
|
|
2018-01-07 12:50:59 +01:00
|
|
|
public static Notification createExportFailedNotification(String text, Context context) {
|
|
|
|
Intent notificationIntent = new Intent(context, SettingsActivity.class);
|
|
|
|
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
|
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
2022-10-09 14:53:04 +02:00
|
|
|
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0,
|
|
|
|
notificationIntent, 0, false);
|
2018-01-07 12:50:59 +01:00
|
|
|
|
2018-02-14 21:27:24 +01:00
|
|
|
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
2018-01-07 12:50:59 +01:00
|
|
|
.setContentTitle(context.getString(R.string.notif_export_failed_title))
|
|
|
|
.setContentText(text)
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
.setSmallIcon(R.drawable.ic_notification)
|
|
|
|
.setPriority(Notification.PRIORITY_HIGH)
|
|
|
|
.setOngoing(false);
|
|
|
|
|
|
|
|
return nb.build();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void updateExportFailedNotification(String text, Context context) {
|
|
|
|
if (GBEnvironment.env().isLocalTest()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Notification notification = createExportFailedNotification(text, context);
|
2021-05-14 18:30:54 +02:00
|
|
|
notify(NOTIFICATION_ID_EXPORT_FAILED, notification, context);
|
2018-01-07 12:50:59 +01:00
|
|
|
}
|
|
|
|
|
2015-09-19 23:32:10 +02:00
|
|
|
public static void assertThat(boolean condition, String errorMessage) {
|
|
|
|
if (!condition) {
|
|
|
|
throw new AssertionError(errorMessage);
|
|
|
|
}
|
|
|
|
}
|
2019-09-16 22:25:58 +02:00
|
|
|
|
|
|
|
public static void signalActivityDataFinish() {
|
|
|
|
Intent intent = new Intent(GBApplication.ACTION_NEW_DATA);
|
|
|
|
LocalBroadcastManager.getInstance(GBApplication.getContext()).sendBroadcast(intent);
|
|
|
|
}
|
2022-08-27 15:12:47 +02:00
|
|
|
|
|
|
|
public static boolean checkPermission(final Context context, final String permission) {
|
|
|
|
return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
|
|
|
}
|
2015-04-13 01:01:52 +02:00
|
|
|
}
|