mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-26 09:37:33 +01:00
Fossil HR: added "last notification" widget
This commit is contained in:
parent
0affbc60cc
commit
7939607beb
@ -451,6 +451,8 @@ public class QHybridSupport extends QHybridBaseSupport {
|
|||||||
public void onDeleteNotification(int id) {
|
public void onDeleteNotification(int id) {
|
||||||
super.onDeleteNotification(id);
|
super.onDeleteNotification(id);
|
||||||
|
|
||||||
|
this.watchAdapter.onDeleteNotification(id);
|
||||||
|
|
||||||
showNotificationsByAllActive(true);
|
showNotificationsByAllActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,4 +135,7 @@ public abstract class WatchAdapter {
|
|||||||
|
|
||||||
public void setBackgroundImage(byte[] pixels) {
|
public void setBackgroundImage(byte[] pixels) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onDeleteNotification(int id) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fos
|
|||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -43,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HRConfigActivity;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
|
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||||
@ -66,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.buttons.ButtonConfigurationPutRequest;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationGetRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationGetRequest;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.configuration.ConfigurationPutRequest;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFile;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FirmwareFilePutRequest;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FirmwareFilePutRequest;
|
||||||
@ -113,6 +117,9 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
|
|
||||||
private boolean saveRawActivityFiles = false;
|
private boolean saveRawActivityFiles = false;
|
||||||
|
|
||||||
|
HashMap<String, Bitmap> appIconCache = new HashMap<>();
|
||||||
|
String lastPostedApp = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
saveRawActivityFiles = getDeviceSpecificPreferences().getBoolean("save_raw_activity_files", false);
|
saveRawActivityFiles = getDeviceSpecificPreferences().getBoolean("save_raw_activity_files", false);
|
||||||
@ -299,7 +306,8 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
negotiateSymmetricKey();
|
negotiateSymmetricKey();
|
||||||
ArrayList<Widget> systemWidgets = new ArrayList<>(widgets.size());
|
ArrayList<Widget> systemWidgets = new ArrayList<>(widgets.size());
|
||||||
for (Widget widget : this.widgets) {
|
for (Widget widget : this.widgets) {
|
||||||
if (!(widget instanceof CustomWidget)) systemWidgets.add(widget);
|
if (!(widget instanceof CustomWidget) && !widget.getWidgetType().isCustom())
|
||||||
|
systemWidgets.add(widget);
|
||||||
}
|
}
|
||||||
queueWrite(new WidgetsPutRequest(systemWidgets.toArray(new Widget[0]), this));
|
queueWrite(new WidgetsPutRequest(systemWidgets.toArray(new Widget[0]), this));
|
||||||
}
|
}
|
||||||
@ -337,7 +345,41 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
for (int i = 0; i < this.widgets.size(); i++) {
|
for (int i = 0; i < this.widgets.size(); i++) {
|
||||||
Widget w = widgets.get(i);
|
Widget w = widgets.get(i);
|
||||||
if (!(w instanceof CustomWidget)) {
|
if (!(w instanceof CustomWidget)) {
|
||||||
if (drawCircles) {
|
if (w.getWidgetType() == Widget.WidgetType.LAST_NOTIFICATION) {
|
||||||
|
Bitmap widgetBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas widgetCanvas = new Canvas(widgetBitmap);
|
||||||
|
if (drawCircles) {
|
||||||
|
widgetCanvas.drawBitmap(circleBitmap, 0, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Paint p = new Paint();
|
||||||
|
p.setStyle(Paint.Style.FILL);
|
||||||
|
p.setTextSize(10);
|
||||||
|
p.setColor(Color.WHITE);
|
||||||
|
|
||||||
|
if (this.lastPostedApp != null) {
|
||||||
|
|
||||||
|
Bitmap icon = appIconCache.get(this.lastPostedApp);
|
||||||
|
|
||||||
|
if (icon != null) {
|
||||||
|
|
||||||
|
widgetCanvas.drawBitmap(
|
||||||
|
icon,
|
||||||
|
(float) (38 - (icon.getWidth() / 2.0)),
|
||||||
|
(float) (38 - (icon.getHeight() / 2.0)),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetImages.add(AssetImageFactory.createAssetImage(
|
||||||
|
widgetBitmap,
|
||||||
|
true,
|
||||||
|
w.getAngle(),
|
||||||
|
w.getDistance(),
|
||||||
|
1
|
||||||
|
));
|
||||||
|
} else if (drawCircles) {
|
||||||
widgetImages.add(AssetImageFactory.createAssetImage(
|
widgetImages.add(AssetImageFactory.createAssetImage(
|
||||||
circleBitmap,
|
circleBitmap,
|
||||||
true,
|
true,
|
||||||
@ -348,7 +390,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
;
|
|
||||||
CustomWidget widget = (CustomWidget) w;
|
CustomWidget widget = (CustomWidget) w;
|
||||||
|
|
||||||
Bitmap widgetBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888);
|
Bitmap widgetBitmap = Bitmap.createBitmap(76, 76, Bitmap.Config.ARGB_8888);
|
||||||
@ -411,9 +453,19 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
|
|
||||||
AssetImage[] images = widgetImages.toArray(new AssetImage[0]);
|
AssetImage[] images = widgetImages.toArray(new AssetImage[0]);
|
||||||
|
|
||||||
if(images.length > 0) {
|
ArrayList<AssetImage> pushFiles = new ArrayList<>(4);
|
||||||
|
imgloop:
|
||||||
|
for (AssetImage image : images) {
|
||||||
|
for (AssetImage pushedImage : pushFiles) {
|
||||||
|
// no need to send same file multiple times, filtering by name since name is hash
|
||||||
|
if (image.getFileName().equals(pushedImage.getFileName())) continue imgloop;
|
||||||
|
}
|
||||||
|
pushFiles.add(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pushFiles.size() > 0) {
|
||||||
queueWrite(new AssetFilePutRequest(
|
queueWrite(new AssetFilePutRequest(
|
||||||
images,
|
pushFiles.toArray(new AssetImage[0]),
|
||||||
(byte) 0x00,
|
(byte) 0x00,
|
||||||
this
|
this
|
||||||
));
|
));
|
||||||
@ -505,7 +557,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
syncSettings();
|
syncSettings();
|
||||||
|
|
||||||
queueWrite(new VerifyPrivateKeyRequest(this.getSecretKey(), this));
|
queueWrite(new VerifyPrivateKeyRequest(this.getSecretKey(), this));
|
||||||
queueWrite(new FileLookupRequest((byte) 0x01, this){
|
queueWrite(new FileLookupRequest((byte) 0x01, this) {
|
||||||
@Override
|
@Override
|
||||||
public void handleFileLookup(final short fileHandle) {
|
public void handleFileLookup(final short fileHandle) {
|
||||||
queueWrite(new FileEncryptedGetRequest(fileHandle, FossilHRWatchAdapter.this) {
|
queueWrite(new FileEncryptedGetRequest(fileHandle, FossilHRWatchAdapter.this) {
|
||||||
@ -519,14 +571,14 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
|
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
|
||||||
|
|
||||||
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
||||||
Long deviceId = DBHelper.getDevice(getDeviceSupport(). getDevice(), dbHandler.getDaoSession()).getId();
|
Long deviceId = DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession()).getId();
|
||||||
for(int i = 0; i < entries.size(); i++){
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId);
|
samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.addGBActivitySamples(samples);
|
provider.addGBActivitySamples(samples);
|
||||||
|
|
||||||
if(saveRawActivityFiles) {
|
if (saveRawActivityFiles) {
|
||||||
writeFile(String.valueOf(System.currentTimeMillis()), fileData);
|
writeFile(String.valueOf(System.currentTimeMillis()), fileData);
|
||||||
}
|
}
|
||||||
queueWrite(new FileDeleteRequest(fileHandle));
|
queueWrite(new FileDeleteRequest(fileHandle));
|
||||||
@ -542,9 +594,9 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
|
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
|
||||||
if(error == FILE_LOOKUP_ERROR.FILE_EMPTY){
|
if (error == FILE_LOOKUP_ERROR.FILE_EMPTY) {
|
||||||
GB.toast("activity file empty yet", Toast.LENGTH_LONG, GB.ERROR);
|
GB.toast("activity file empty yet", Toast.LENGTH_LONG, GB.ERROR);
|
||||||
}else{
|
} else {
|
||||||
throw new RuntimeException("strange lookup stuff");
|
throw new RuntimeException("strange lookup stuff");
|
||||||
}
|
}
|
||||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||||
@ -552,7 +604,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeFile(String fileName, byte[] value){
|
private void writeFile(String fileName, byte[] value) {
|
||||||
File activityDir = new File(getContext().getExternalFilesDir(null), "activity_hr");
|
File activityDir = new File(getContext().getExternalFilesDir(null), "activity_hr");
|
||||||
activityDir.mkdir();
|
activityDir.mkdir();
|
||||||
File f = new File(activityDir, fileName);
|
File f = new File(activityDir, fileName);
|
||||||
@ -579,12 +631,14 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean playRawNotification(NotificationSpec notificationSpec) {
|
public boolean playRawNotification(NotificationSpec notificationSpec) {
|
||||||
|
String sourceAppId = notificationSpec.sourceAppId;
|
||||||
|
|
||||||
String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
|
String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (NotificationHRConfiguration configuration : this.notificationConfigurations) {
|
for (NotificationHRConfiguration configuration : this.notificationConfigurations) {
|
||||||
if (configuration.getPackageName().equals(notificationSpec.sourceAppId)) {
|
if (configuration.getPackageName().equals(sourceAppId)) {
|
||||||
queueWrite(new PlayTextNotificationRequest(notificationSpec.sourceAppId, senderOrTitle, notificationSpec.body, this));
|
queueWrite(new PlayTextNotificationRequest(sourceAppId, senderOrTitle, notificationSpec.body, this));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -592,9 +646,43 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sourceAppId != null) {
|
||||||
|
if (!sourceAppId.equals(this.lastPostedApp)) {
|
||||||
|
if (appIconCache.get(sourceAppId) == null) {
|
||||||
|
try {
|
||||||
|
PackageManager pm = getContext().getPackageManager();
|
||||||
|
Drawable icon = pm.getApplicationIcon(sourceAppId);
|
||||||
|
|
||||||
|
Bitmap iconBitmap = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888);
|
||||||
|
icon.setBounds(0, 0, 40, 40);
|
||||||
|
icon.draw(new Canvas(iconBitmap));
|
||||||
|
|
||||||
|
appIconCache.put(sourceAppId, iconBitmap);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.lastPostedApp = sourceAppId;
|
||||||
|
renderWidgets();
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeleteNotification(int id) {
|
||||||
|
super.onDeleteNotification(id);
|
||||||
|
|
||||||
|
// only delete app icon when no notification of said app is present
|
||||||
|
for (String app : NotificationListener.notificationStack) {
|
||||||
|
if (app.equals(this.lastPostedApp)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastPostedApp = null;
|
||||||
|
renderWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFindDevice(boolean start) {
|
public void onFindDevice(boolean start) {
|
||||||
if (start) {
|
if (start) {
|
||||||
|
@ -21,6 +21,10 @@ public class Widget implements Serializable {
|
|||||||
this.fontColor = fontColor;
|
this.fontColor = fontColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WidgetType getWidgetType() {
|
||||||
|
return widgetType;
|
||||||
|
}
|
||||||
|
|
||||||
public int getAngle() {
|
public int getAngle() {
|
||||||
return angle;
|
return angle;
|
||||||
}
|
}
|
||||||
@ -47,8 +51,11 @@ public class Widget implements Serializable {
|
|||||||
JSONObject object = new JSONObject();
|
JSONObject object = new JSONObject();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String type;
|
||||||
|
if(widgetType == null) type = "Custom widget";
|
||||||
|
else type = widgetType.getIdentifier();
|
||||||
object
|
object
|
||||||
.put("name", widgetType.getIdentifier())
|
.put("name", type)
|
||||||
.put("pos",
|
.put("pos",
|
||||||
new JSONObject()
|
new JSONObject()
|
||||||
.put("angle", angle)
|
.put("angle", angle)
|
||||||
@ -75,14 +82,23 @@ public class Widget implements Serializable {
|
|||||||
CALORIES("caloriesSSE", R.string.hr_widget_calories),
|
CALORIES("caloriesSSE", R.string.hr_widget_calories),
|
||||||
BATTERY("batterySSE", R.string.hr_widget_battery),
|
BATTERY("batterySSE", R.string.hr_widget_battery),
|
||||||
WEATHER("weatherSSE", R.string.hr_widget_weather),
|
WEATHER("weatherSSE", R.string.hr_widget_weather),
|
||||||
|
LAST_NOTIFICATION("last_notification", R.string.hr_widget_last_notification, true),
|
||||||
NOTHING(null, R.string.hr_widget_nothing);
|
NOTHING(null, R.string.hr_widget_nothing);
|
||||||
|
|
||||||
private String identifier;
|
private String identifier;
|
||||||
private int stringResource;
|
private int stringResource;
|
||||||
|
private boolean custom;
|
||||||
|
|
||||||
WidgetType(String identifier, int stringResource) {
|
WidgetType(String identifier, int stringResource) {
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
this.stringResource = stringResource;
|
this.stringResource = stringResource;
|
||||||
|
this.custom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetType(String identifier, int stringResource, boolean custom) {
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.stringResource = stringResource;
|
||||||
|
this.custom = custom;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static WidgetType fromJsonIdentifier(String jsonIdentifier){
|
public static WidgetType fromJsonIdentifier(String jsonIdentifier){
|
||||||
@ -99,5 +115,9 @@ public class Widget implements Serializable {
|
|||||||
public String getIdentifier() {
|
public String getIdentifier() {
|
||||||
return this.identifier;
|
return this.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCustom() {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -906,4 +906,5 @@
|
|||||||
<string name="bip_prefs_shotcuts_summary">Wähle die Verknüpfungen auf dem Band-Bildschirm</string>
|
<string name="bip_prefs_shotcuts_summary">Wähle die Verknüpfungen auf dem Band-Bildschirm</string>
|
||||||
<string name="menuitem_unknown">Unbekannt</string>
|
<string name="menuitem_unknown">Unbekannt</string>
|
||||||
<string name="bip_prefs_shortcuts">Verknüpfungen</string>
|
<string name="bip_prefs_shortcuts">Verknüpfungen</string>
|
||||||
|
<string name="hr_widget_last_notification">Letzte Benachrichtigung</string>
|
||||||
</resources>
|
</resources>
|
@ -861,6 +861,7 @@
|
|||||||
<string name="error_no_location_access">Location access must be granted and enabled for scanning to work properly</string>
|
<string name="error_no_location_access">Location access must be granted and enabled for scanning to work properly</string>
|
||||||
<string name="pref_qhybrid_title_widget_draw_circles">Draw widget circles</string>
|
<string name="pref_qhybrid_title_widget_draw_circles">Draw widget circles</string>
|
||||||
<string name="pref_qhybrid_save_raw_activity_files">Save raw activity files</string>
|
<string name="pref_qhybrid_save_raw_activity_files">Save raw activity files</string>
|
||||||
|
<string name="hr_widget_last_notification">Last notification</string>
|
||||||
<plurals name="widget_alarm_target_hours">
|
<plurals name="widget_alarm_target_hours">
|
||||||
<item quantity="one">%d hour</item>
|
<item quantity="one">%d hour</item>
|
||||||
<item quantity="two">%d hours</item>
|
<item quantity="two">%d hours</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user