From c1145e1244f04675a229e054c87db33ac6a9d8c0 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Wed, 21 Aug 2019 23:24:51 +0200 Subject: [PATCH 001/154] Mi Band 4: Support flashing the V2 font that came with beta FW 1.0.6.00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only tested with 1.0.6.00 It now contains new characters like äöüß and others. --- .../service/devices/huami/miband4/MiBand4FirmwareInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java index 5b7f3fcee..38987a3b2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java @@ -65,7 +65,7 @@ public class MiBand4FirmwareInfo extends HuamiFirmwareInfo { return HuamiFirmwareType.WATCHFACE; } if (ArrayUtils.startsWith(bytes, NEWFT_HEADER)) { - if (bytes[10] == 0x03) { + if (bytes[10] == 0x03 || bytes[10] == 0x06) { return HuamiFirmwareType.FONT; } } From aec3d21216c20dec0338e1c2ea2da4c67df51e41 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 22 Aug 2019 10:20:45 +0200 Subject: [PATCH 002/154] Mi Band 4: remove unsupported DND setting from settings menu --- .../gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java index 13468b851..31c0796d6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java @@ -90,7 +90,6 @@ public class MiBand4Coordinator extends HuamiCoordinator { R.xml.devicesettings_miband3, R.xml.devicesettings_dateformat, R.xml.devicesettings_nightmode, - R.xml.devicesettings_donotdisturb_withauto, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_swipeunlock, R.xml.devicesettings_pairingkey From 75c99158aeb464a36076e0724211f9d6ba820cf5 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 22 Aug 2019 11:22:29 +0200 Subject: [PATCH 003/154] collect changes so far into changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b1208a4..4406b2393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ### Changelog +#### NEXT +* Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time +* Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now) +* Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect) +* Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise" +* Mi Band 4: Support flashing the V2 font +* Mi Band 4: remove unsupported DND setting from settings menu +* Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities +* Amazfit Bip: Fix sharing GPX files for some Apps +* Pebble: Use Rebble Store URI +* Add Averages to Charts +* Allow togging between weekly and monthly charts + #### Version 0.35.2 * Mi Band 1/2: Crash when updating firmware while phone is set to Spanish * Mi Band 4: Enable music info support (displays now on the band) From 5f998d8a95b1b399e7f1f4ab334250008e2e5c5c Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 22 Aug 2019 21:19:03 +0200 Subject: [PATCH 004/154] add stripped down version of weather part of lineage sdk Makes it possible to use the lineage weather provider without binary jar (This is based on 63a590625c6c76f82e5ef43408a52238b2b34e43 of https://github.com/LineageOS/android_lineage-sdk) --- .../weather/ILineageWeatherManager.aidl | 31 + .../weather/IRequestInfoListener.aidl | 30 + ...IWeatherServiceProviderChangeListener.aidl | 22 + .../aidl/lineageos/weather/RequestInfo.aidl | 19 + .../aidl/lineageos/weather/WeatherInfo.aidl | 19 + .../lineageos/weather/WeatherLocation.aidl | 19 + .../IWeatherProviderService.aidl | 28 + .../IWeatherProviderServiceClient.aidl | 25 + .../weatherservice/ServiceRequestResult.aidl | 19 + .../app/LineageContextConstants.java | 31 + app/src/main/java/lineageos/os/Build.java | 32 + app/src/main/java/lineageos/os/Concierge.java | 153 +++++ .../lineageos/providers/WeatherContract.java | 245 +++++++ .../weather/LineageWeatherManager.java | 435 ++++++++++++ .../java/lineageos/weather/RequestInfo.java | 379 +++++++++++ .../java/lineageos/weather/WeatherInfo.java | 642 ++++++++++++++++++ .../lineageos/weather/WeatherLocation.java | 274 ++++++++ .../lineageos/weather/util/WeatherUtils.java | 84 +++ .../weatherservice/ServiceRequest.java | 161 +++++ .../weatherservice/ServiceRequestResult.java | 197 ++++++ .../WeatherProviderService.java | 181 +++++ 21 files changed, 3026 insertions(+) create mode 100644 app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl create mode 100644 app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl create mode 100644 app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl create mode 100644 app/src/main/aidl/lineageos/weather/RequestInfo.aidl create mode 100644 app/src/main/aidl/lineageos/weather/WeatherInfo.aidl create mode 100644 app/src/main/aidl/lineageos/weather/WeatherLocation.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl create mode 100644 app/src/main/java/lineageos/app/LineageContextConstants.java create mode 100644 app/src/main/java/lineageos/os/Build.java create mode 100644 app/src/main/java/lineageos/os/Concierge.java create mode 100644 app/src/main/java/lineageos/providers/WeatherContract.java create mode 100644 app/src/main/java/lineageos/weather/LineageWeatherManager.java create mode 100644 app/src/main/java/lineageos/weather/RequestInfo.java create mode 100755 app/src/main/java/lineageos/weather/WeatherInfo.java create mode 100644 app/src/main/java/lineageos/weather/WeatherLocation.java create mode 100644 app/src/main/java/lineageos/weather/util/WeatherUtils.java create mode 100644 app/src/main/java/lineageos/weatherservice/ServiceRequest.java create mode 100644 app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java create mode 100644 app/src/main/java/lineageos/weatherservice/WeatherProviderService.java diff --git a/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl b/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl new file mode 100644 index 000000000..d12e8bbd4 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import lineageos.weather.IWeatherServiceProviderChangeListener; +import lineageos.weather.RequestInfo; + +interface ILineageWeatherManager { + oneway void updateWeather(in RequestInfo info); + oneway void lookupCity(in RequestInfo info); + oneway void registerWeatherServiceProviderChangeListener( + in IWeatherServiceProviderChangeListener listener); + oneway void unregisterWeatherServiceProviderChangeListener( + in IWeatherServiceProviderChangeListener listener); + String getActiveWeatherServiceProviderLabel(); + oneway void cancelRequest(int requestId); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl b/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl new file mode 100644 index 000000000..11916f3cf --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import lineageos.weather.RequestInfo; +import lineageos.weather.WeatherInfo; +import lineageos.weather.WeatherLocation; + +import java.util.List; + +interface IRequestInfoListener { + void onWeatherRequestCompleted(in RequestInfo requestInfo, int status, + in WeatherInfo weatherInfo); + void onLookupCityRequestCompleted(in RequestInfo requestInfo, int status, + in List weatherLocation); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl b/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl new file mode 100644 index 000000000..12ad2ff8f --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +/** @hide */ +oneway interface IWeatherServiceProviderChangeListener { + void onWeatherServiceProviderChanged(String providerLabel); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/RequestInfo.aidl b/app/src/main/aidl/lineageos/weather/RequestInfo.aidl new file mode 100644 index 000000000..bdc3ecc64 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/RequestInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable RequestInfo; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl b/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl new file mode 100644 index 000000000..16cbb599e --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanongenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable WeatherInfo; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl b/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl new file mode 100644 index 000000000..d19e8bce7 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable WeatherLocation; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl new file mode 100644 index 000000000..fb3bc5ec3 --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import lineageos.weatherservice.IWeatherProviderServiceClient; +import lineageos.weather.RequestInfo; + +interface IWeatherProviderService { + void processWeatherUpdateRequest(in RequestInfo request); + void processCityNameLookupRequest(in RequestInfo request); + void setServiceClient(in IWeatherProviderServiceClient client); + void cancelOngoingRequests(); + void cancelRequest(int requestId); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl new file mode 100644 index 000000000..2f54eddc1 --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import lineageos.weather.RequestInfo; +import lineageos.weatherservice.ServiceRequestResult; + +interface IWeatherProviderServiceClient { + void setServiceRequestState(in RequestInfo requestInfo, in ServiceRequestResult result, + int state); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl b/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl new file mode 100644 index 000000000..ece4f47fc --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +parcelable ServiceRequestResult; \ No newline at end of file diff --git a/app/src/main/java/lineageos/app/LineageContextConstants.java b/app/src/main/java/lineageos/app/LineageContextConstants.java new file mode 100644 index 000000000..a5ab9d3e3 --- /dev/null +++ b/app/src/main/java/lineageos/app/LineageContextConstants.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.app; + + +public final class LineageContextConstants { + + private LineageContextConstants() { + // Empty constructor + } + + public static final String LINEAGE_WEATHER_SERVICE = "lineageweather"; + + public static class Features { + public static final String WEATHER_SERVICES = "org.lineageos.weather"; + } +} diff --git a/app/src/main/java/lineageos/os/Build.java b/app/src/main/java/lineageos/os/Build.java new file mode 100644 index 000000000..61d475b7b --- /dev/null +++ b/app/src/main/java/lineageos/os/Build.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.os; + + +public class Build { + public static class LINEAGE_VERSION_CODES { + public static final int APRICOT = 1; + public static final int BOYSENBERRY = 2; + public static final int CANTALOUPE = 3; + public static final int DRAGON_FRUIT = 4; + public static final int ELDERBERRY = 5; + public static final int FIG = 6; + public static final int GUAVA = 7; + public static final int HACKBERRY = 8; + public static final int ILAMA = 9; + } +} diff --git a/app/src/main/java/lineageos/os/Concierge.java b/app/src/main/java/lineageos/os/Concierge.java new file mode 100644 index 000000000..d4f9b4e22 --- /dev/null +++ b/app/src/main/java/lineageos/os/Concierge.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.os; + +import android.os.Parcel; + +import lineageos.os.Build.LINEAGE_VERSION_CODES; + +/** + * Simply, Concierge handles your parcels and makes sure they get marshalled and unmarshalled + * correctly when cross IPC boundaries even when there is a version mismatch between the client + * sdk level and the framework implementation. + * + *

On incoming parcel (to be unmarshalled): + * + *

+ *     ParcelInfo incomingParcelInfo = Concierge.receiveParcel(incomingParcel);
+ *     int parcelableVersion = incomingParcelInfo.getParcelVersion();
+ *
+ *     // Do unmarshalling steps here iterating over every plausible version
+ *
+ *     // Complete the process
+ *     incomingParcelInfo.complete();
+ * 
+ * + *

On outgoing parcel (to be marshalled): + * + *

+ *     ParcelInfo outgoingParcelInfo = Concierge.prepareParcel(incomingParcel);
+ *
+ *     // Do marshalling steps here iterating over every plausible version
+ *
+ *     // Complete the process
+ *     outgoingParcelInfo.complete();
+ * 
+ */ +public final class Concierge { + + /** Not instantiable */ + private Concierge() { + // Don't instantiate + } + + /** + * Since there might be a case where new versions of the lineage framework use applications running + * old versions of the protocol (and thus old versions of this class), we need a versioning + * system for the parcels sent between the core framework and its sdk users. + * + * This parcelable version should be the latest version API version listed in + * {@link LINEAGE_VERSION_CODES} + * @hide + */ + public static final int PARCELABLE_VERSION = LINEAGE_VERSION_CODES.ILAMA; + + /** + * Tell the concierge to receive our parcel, so we can get information from it. + * + * MUST CALL {@link ParcelInfo#complete()} AFTER UNMARSHALLING. + * + * @param parcel Incoming parcel to be unmarshalled + * @return {@link ParcelInfo} containing parcel information, specifically the version. + */ + public static ParcelInfo receiveParcel(Parcel parcel) { + return new ParcelInfo(parcel); + } + + /** + * Prepare a parcel for the Concierge. + * + * MUST CALL {@link ParcelInfo#complete()} AFTER MARSHALLING. + * + * @param parcel Outgoing parcel to be marshalled + * @return {@link ParcelInfo} containing parcel information, specifically the version. + */ + public static ParcelInfo prepareParcel(Parcel parcel) { + return new ParcelInfo(parcel, PARCELABLE_VERSION); + } + + /** + * Parcel header info specific to the Parcel object that is passed in via + * {@link #prepareParcel(Parcel)} or {@link #receiveParcel(Parcel)}. The exposed method + * of {@link #getParcelVersion()} gets the api level of the parcel object. + */ + public final static class ParcelInfo { + private Parcel mParcel; + private int mParcelableVersion; + private int mParcelableSize; + private int mStartPosition; + private int mSizePosition; + private boolean mCreation = false; + + ParcelInfo(Parcel parcel) { + mCreation = false; + mParcel = parcel; + mParcelableVersion = parcel.readInt(); + mParcelableSize = parcel.readInt(); + mStartPosition = parcel.dataPosition(); + } + + ParcelInfo(Parcel parcel, int parcelableVersion) { + mCreation = true; + mParcel = parcel; + mParcelableVersion = parcelableVersion; + + // Write parcelable version, make sure to define explicit changes + // within {@link #PARCELABLE_VERSION); + mParcel.writeInt(mParcelableVersion); + + // Inject a placeholder that will store the parcel size from this point on + // (not including the size itself). + mSizePosition = parcel.dataPosition(); + mParcel.writeInt(0); + mStartPosition = parcel.dataPosition(); + } + + /** + * Get the parcel version from the {@link Parcel} received by the Concierge. + * @return {@link #PARCELABLE_VERSION} of the {@link Parcel} + */ + public int getParcelVersion() { + return mParcelableVersion; + } + + /** + * Complete the {@link ParcelInfo} for the Concierge. + */ + public void complete() { + if (mCreation) { + // Go back and write size + mParcelableSize = mParcel.dataPosition() - mStartPosition; + mParcel.setDataPosition(mSizePosition); + mParcel.writeInt(mParcelableSize); + mParcel.setDataPosition(mStartPosition + mParcelableSize); + } else { + mParcel.setDataPosition(mStartPosition + mParcelableSize); + } + } + } +} diff --git a/app/src/main/java/lineageos/providers/WeatherContract.java b/app/src/main/java/lineageos/providers/WeatherContract.java new file mode 100644 index 000000000..b352db526 --- /dev/null +++ b/app/src/main/java/lineageos/providers/WeatherContract.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.providers; + +import android.net.Uri; + +/** + * The contract between the weather provider and applications. + */ +public class WeatherContract { + + /** + * The authority of the weather content provider + */ + public static final String AUTHORITY = "org.lineageos.weather"; + + /** + * A content:// style uri to the authority for the weather provider + */ + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + public static class WeatherColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "weather"); + + public static final Uri CURRENT_AND_FORECAST_WEATHER_URI + = Uri.withAppendedPath(CONTENT_URI, "current_and_forecast"); + public static final Uri CURRENT_WEATHER_URI + = Uri.withAppendedPath(CONTENT_URI, "current"); + public static final Uri FORECAST_WEATHER_URI + = Uri.withAppendedPath(CONTENT_URI, "forecast"); + + /** + * The city name + *

Type: TEXT

+ */ + public static final String CURRENT_CITY = "city"; + + /** + * A Valid {@link WeatherCode} + *

Type: INTEGER

+ */ + public static final String CURRENT_CONDITION_CODE = "condition_code"; + + + /** + * A localized string mapped to the current weather condition code. Note that, if no + * locale is found, the string will be in english + *

Type: TEXT

+ */ + public static final String CURRENT_CONDITION = "condition"; + + /** + * The current weather temperature + *

Type: DOUBLE

+ */ + public static final String CURRENT_TEMPERATURE = "temperature"; + + /** + * The unit in which current temperature is reported + *

Type: INTEGER

+ * Can be one of the following: + *
    + *
  • {@link TempUnit#CELSIUS}
  • + *
  • {@link TempUnit#FAHRENHEIT}
  • + *
+ */ + public static final String CURRENT_TEMPERATURE_UNIT = "temperature_unit"; + + /** + * The current weather humidity + *

Type: DOUBLE

+ */ + public static final String CURRENT_HUMIDITY = "humidity"; + + /** + * The current wind direction (in degrees) + *

Type: DOUBLE

+ */ + public static final String CURRENT_WIND_DIRECTION = "wind_direction"; + + /** + * The current wind speed + *

Type: DOUBLE

+ */ + public static final String CURRENT_WIND_SPEED = "wind_speed"; + + /** + * The unit in which the wind speed is reported + *

Type: INTEGER

+ * Can be one of the following: + *
    + *
  • {@link WindSpeedUnit#KPH}
  • + *
  • {@link WindSpeedUnit#MPH}
  • + *
+ */ + public static final String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit"; + + /** + * The timestamp when this weather was reported + *

Type: LONG

+ */ + public static final String CURRENT_TIMESTAMP = "timestamp"; + + /** + * Today's high temperature. + *

Type: DOUBLE

+ */ + public static final String TODAYS_HIGH_TEMPERATURE = "todays_high"; + + /** + * Today's low temperature. + *

Type: DOUBLE

+ */ + public static final String TODAYS_LOW_TEMPERATURE = "todays_low"; + + /** + * The forecasted low temperature + *

Type: DOUBLE

+ */ + public static final String FORECAST_LOW = "forecast_low"; + + /** + * The forecasted high temperature + *

Type: DOUBLE

+ */ + public static final String FORECAST_HIGH = "forecast_high"; + + /** + * A localized string mapped to the forecasted weather condition code. Note that, if no + * locale is found, the string will be in english + *

Type: TEXT

+ */ + public static final String FORECAST_CONDITION = "forecast_condition"; + + /** + * The code identifying the forecasted weather condition. + * @see #CURRENT_CONDITION_CODE + */ + public static final String FORECAST_CONDITION_CODE = "forecast_condition_code"; + + /** + * Temperature units + */ + public static final class TempUnit { + private TempUnit() {} + public final static int CELSIUS = 1; + public final static int FAHRENHEIT = 2; + } + + /** + * Wind speed units + */ + public static final class WindSpeedUnit { + private WindSpeedUnit() {} + /** + * Kilometers per hour + */ + public final static int KPH = 1; + + /** + * Miles per hour + */ + public final static int MPH = 2; + } + + /** + * Weather condition codes + */ + public static final class WeatherCode { + private WeatherCode() {} + + /** + * @hide + */ + public final static int WEATHER_CODE_MIN = 0; + + public final static int TORNADO = 0; + public final static int TROPICAL_STORM = 1; + public final static int HURRICANE = 2; + public final static int SEVERE_THUNDERSTORMS = 3; + public final static int THUNDERSTORMS = 4; + public final static int MIXED_RAIN_AND_SNOW = 5; + public final static int MIXED_RAIN_AND_SLEET = 6; + public final static int MIXED_SNOW_AND_SLEET = 7; + public final static int FREEZING_DRIZZLE = 8; + public final static int DRIZZLE = 9; + public final static int FREEZING_RAIN = 10; + public final static int SHOWERS = 11; + public final static int SNOW_FLURRIES = 12; + public final static int LIGHT_SNOW_SHOWERS = 13; + public final static int BLOWING_SNOW = 14; + public final static int SNOW = 15; + public final static int HAIL = 16; + public final static int SLEET = 17; + public final static int DUST = 18; + public final static int FOGGY = 19; + public final static int HAZE = 20; + public final static int SMOKY = 21; + public final static int BLUSTERY = 22; + public final static int WINDY = 23; + public final static int COLD = 24; + public final static int CLOUDY = 25; + public final static int MOSTLY_CLOUDY_NIGHT = 26; + public final static int MOSTLY_CLOUDY_DAY = 27; + public final static int PARTLY_CLOUDY_NIGHT = 28; + public final static int PARTLY_CLOUDY_DAY = 29; + public final static int CLEAR_NIGHT = 30; + public final static int SUNNY = 31; + public final static int FAIR_NIGHT = 32; + public final static int FAIR_DAY = 33; + public final static int MIXED_RAIN_AND_HAIL = 34; + public final static int HOT = 35; + public final static int ISOLATED_THUNDERSTORMS = 36; + public final static int SCATTERED_THUNDERSTORMS = 37; + public final static int SCATTERED_SHOWERS = 38; + public final static int HEAVY_SNOW = 39; + public final static int SCATTERED_SNOW_SHOWERS = 40; + public final static int PARTLY_CLOUDY = 41; + public final static int THUNDERSHOWER = 42; + public final static int SNOW_SHOWERS = 43; + public final static int ISOLATED_THUNDERSHOWERS = 44; + + /** + * @hide + */ + public final static int WEATHER_CODE_MAX = 44; + + public final static int NOT_AVAILABLE = 3200; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/lineageos/weather/LineageWeatherManager.java b/app/src/main/java/lineageos/weather/LineageWeatherManager.java new file mode 100644 index 000000000..48c767dce --- /dev/null +++ b/app/src/main/java/lineageos/weather/LineageWeatherManager.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.location.Location; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lineageos.app.LineageContextConstants; +import lineageos.providers.WeatherContract; + +/** + * Provides access to the weather services in the device. + */ +@RequiresApi(api = Build.VERSION_CODES.M) +public class LineageWeatherManager { + + private static ILineageWeatherManager sWeatherManagerService; + private static LineageWeatherManager sInstance; + private Context mContext; + private Map mWeatherUpdateRequestListeners + = Collections.synchronizedMap(new HashMap()); + private Map mLookupNameRequestListeners + = Collections.synchronizedMap(new HashMap()); + private Handler mHandler; + private Set mProviderChangedListeners = new ArraySet<>(); + + private static final String TAG = LineageWeatherManager.class.getSimpleName(); + + + /** + * The different request statuses + */ + public static final class RequestStatus { + + private RequestStatus() {} + + /** + * Request successfully completed + */ + public static final int COMPLETED = 1; + /** + * An error occurred while trying to honor the request + */ + public static final int FAILED = -1; + /** + * The request can't be processed at this time + */ + public static final int SUBMITTED_TOO_SOON = -2; + /** + * Another request is already in progress + */ + public static final int ALREADY_IN_PROGRESS = -3; + /** + * No match found for the query + */ + public static final int NO_MATCH_FOUND = -4; + } + + private LineageWeatherManager(Context context) { + Context appContext = context.getApplicationContext(); + mContext = (appContext != null) ? appContext : context; + sWeatherManagerService = getService(); + + if (context.getPackageManager().hasSystemFeature( + LineageContextConstants.Features.WEATHER_SERVICES) && (sWeatherManagerService == null)) { + Log.wtf(TAG, "Unable to bind the LineageWeatherManagerService"); + } + mHandler = new Handler(appContext.getMainLooper()); + } + + /** + * Gets or creates an instance of the {@link lineageos.weather.LineageWeatherManager} + * @param context + * @return {@link LineageWeatherManager} + */ + public static LineageWeatherManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new LineageWeatherManager(context); + } + return sInstance; + } + + /** + * @hide + */ + @SuppressLint("PrivateApi") + public static ILineageWeatherManager getService() { + if (sWeatherManagerService != null) { + return sWeatherManagerService; + } + + // This is a Gadgetbridge hack + IBinder binder = null; + try { + Class localClass = Class.forName("android.os.ServiceManager"); + Method getService = localClass.getMethod("getService", String.class); + Object result = getService.invoke(localClass, LineageContextConstants.LINEAGE_WEATHER_SERVICE); + if (result != null) { + binder = (IBinder) result; + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + if (binder != null) { + sWeatherManagerService = ILineageWeatherManager.Stub.asInterface(binder); + return sWeatherManagerService; + } + + return null; + } + + /** + * Forces the weather service to request the latest available weather information for + * the supplied {@link android.location.Location} location. + * + * @param location The location you want to get the latest weather data from. + * @param listener {@link WeatherUpdateRequestListener} To be notified once the active weather + * service provider has finished + * processing your request + * @return An integer that identifies the request submitted to the weather service + * Note that this method might return -1 if an error occurred while trying to submit + * the request. + */ + public int requestWeatherUpdate(@NonNull Location location, + @NonNull WeatherUpdateRequestListener listener) { + if (sWeatherManagerService == null) { + return -1; + } + + try { + int tempUnit = WeatherContract.WeatherColumns.TempUnit.CELSIUS; + + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setLocation(location) + .setTemperatureUnit(tempUnit) + .build(); + if (listener != null) mWeatherUpdateRequestListeners.put(info, listener); + sWeatherManagerService.updateWeather(info); + return info.hashCode(); + } catch (RemoteException e) { + return -1; + } + } + + /** + * Forces the weather service to request the latest weather information for the provided + * WeatherLocation. This is the preferred method for requesting a weather update. + * + * @param weatherLocation A {@link lineageos.weather.WeatherLocation} that was previously + * obtained by calling + * {@link #lookupCity(String, LookupCityRequestListener)} + * @param listener {@link WeatherUpdateRequestListener} To be notified once the active weather + * service provider has finished + * processing your request + * @return An integer that identifies the request submitted to the weather service. + * Note that this method might return -1 if an error occurred while trying to submit + * the request. + */ + public int requestWeatherUpdate(@NonNull WeatherLocation weatherLocation, + @NonNull WeatherUpdateRequestListener listener) { + if (sWeatherManagerService == null) { + return -1; + } + + try { + int tempUnit = WeatherContract.WeatherColumns.TempUnit.CELSIUS; + + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setWeatherLocation(weatherLocation) + .setTemperatureUnit(tempUnit) + .build(); + if (listener != null) mWeatherUpdateRequestListeners.put(info, listener); + sWeatherManagerService.updateWeather(info); + return info.hashCode(); + } catch (RemoteException e) { + return -1; + } + } + + /** + * Request the active weather provider service to lookup the supplied city name. + * + * @param city The city name + * @param listener {@link LookupCityRequestListener} To be notified once the request has been + * completed. Upon success, a list of + * {@link lineageos.weather.WeatherLocation} + * will be provided + * @return An integer that identifies the request submitted to the weather service. + * Note that this method might return -1 if an error occurred while trying to submit + * the request. + */ + public int lookupCity(@NonNull String city, @NonNull LookupCityRequestListener listener) { + if (sWeatherManagerService == null) { + return -1; + } + try { + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setCityName(city) + .build(); + if (listener != null) mLookupNameRequestListeners.put(info, listener); + sWeatherManagerService.lookupCity(info); + return info.hashCode(); + } catch (RemoteException e) { + return -1; + } + } + + /** + * Cancels a request that was previously submitted to the weather service. + * @param requestId The ID that you received when the request was submitted + */ + public void cancelRequest(int requestId) { + if (sWeatherManagerService == null) { + return; + } + + try { + sWeatherManagerService.cancelRequest(requestId); + }catch (RemoteException e){ + } + } + + /** + * Registers a {@link WeatherServiceProviderChangeListener} to be notified when a new weather + * service provider becomes active. + * @param listener {@link WeatherServiceProviderChangeListener} to register + */ + public void registerWeatherServiceProviderChangeListener( + @NonNull WeatherServiceProviderChangeListener listener) { + if (sWeatherManagerService == null) return; + + synchronized (mProviderChangedListeners) { + if (mProviderChangedListeners.contains(listener)) { + throw new IllegalArgumentException("Listener already registered"); + } + if (mProviderChangedListeners.size() == 0) { + try { + sWeatherManagerService.registerWeatherServiceProviderChangeListener( + mProviderChangeListener); + } catch (RemoteException e){ + } + } + mProviderChangedListeners.add(listener); + } + } + + /** + * Unregisters a listener + * @param listener A previously registered {@link WeatherServiceProviderChangeListener} + */ + public void unregisterWeatherServiceProviderChangeListener( + @NonNull WeatherServiceProviderChangeListener listener) { + if (sWeatherManagerService == null) return; + + synchronized (mProviderChangedListeners) { + if (!mProviderChangedListeners.contains(listener)) { + throw new IllegalArgumentException("Listener was never registered"); + } + mProviderChangedListeners.remove(listener); + if (mProviderChangedListeners.size() == 0) { + try { + sWeatherManagerService.unregisterWeatherServiceProviderChangeListener( + mProviderChangeListener); + } catch(RemoteException e){ + } + } + } + } + + /** + * Gets the service's label as declared by the active weather service provider in its manifest + * @return the service's label + */ + public String getActiveWeatherServiceProviderLabel() { + if (sWeatherManagerService == null) return null; + + try { + return sWeatherManagerService.getActiveWeatherServiceProviderLabel(); + } catch(RemoteException e){ + } + return null; + } + + private final IWeatherServiceProviderChangeListener mProviderChangeListener = + new IWeatherServiceProviderChangeListener.Stub() { + @Override + public void onWeatherServiceProviderChanged(final String providerName) { + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mProviderChangedListeners) { + List deadListeners + = new ArrayList<>(); + for (WeatherServiceProviderChangeListener listener + : mProviderChangedListeners) { + try { + listener.onWeatherServiceProviderChanged(providerName); + } catch (Throwable e) { + deadListeners.add(listener); + } + } + if (deadListeners.size() > 0) { + for (WeatherServiceProviderChangeListener listener : deadListeners) { + mProviderChangedListeners.remove(listener); + } + } + } + } + }); + } + }; + + private final IRequestInfoListener mRequestInfoListener = new IRequestInfoListener.Stub() { + + @Override + public void onWeatherRequestCompleted(final RequestInfo requestInfo, final int status, + final WeatherInfo weatherInfo) { + final WeatherUpdateRequestListener listener + = mWeatherUpdateRequestListeners.remove(requestInfo); + if (listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + listener.onWeatherRequestCompleted(status, weatherInfo); + } + }); + } + } + + @Override + public void onLookupCityRequestCompleted(RequestInfo requestInfo, final int status, + final List weatherLocations) { + + final LookupCityRequestListener listener + = mLookupNameRequestListeners.remove(requestInfo); + if (listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + listener.onLookupCityRequestCompleted(status, weatherLocations); + } + }); + } + } + }; + + /** + * Interface used to receive notifications upon completion of a weather update request + */ + public interface WeatherUpdateRequestListener { + /** + * This method will be called when the weather service provider has finished processing the + * request + * + * @param status See {@link RequestStatus} + * + * @param weatherInfo A fully populated {@link WeatherInfo} if state is + * {@link RequestStatus#COMPLETED}, null otherwise + */ + void onWeatherRequestCompleted(int status, WeatherInfo weatherInfo); + } + + /** + * Interface used to receive notifications upon completion of a request to lookup a city name + */ + public interface LookupCityRequestListener { + /** + * This method will be called when the weather service provider has finished processing the + * request. + * + * @param status See {@link RequestStatus} + * + * @param locations A list of {@link WeatherLocation} if the status is + * {@link RequestStatus#COMPLETED}, null otherwise + */ + void onLookupCityRequestCompleted(int status, List locations); + } + + /** + * Interface used to be notified when the user changes the weather service provider + */ + public interface WeatherServiceProviderChangeListener { + /** + * This method will be called when a new weather service provider becomes active in the + * system. The parameter can be null when + *

The user removed the active weather service provider from the system

+ *

The active weather provider was disabled.

+ * + * @param providerLabel The label as declared on the weather service provider manifest + */ + void onWeatherServiceProviderChanged(String providerLabel); + } +} diff --git a/app/src/main/java/lineageos/weather/RequestInfo.java b/app/src/main/java/lineageos/weather/RequestInfo.java new file mode 100644 index 000000000..a236bef6b --- /dev/null +++ b/app/src/main/java/lineageos/weather/RequestInfo.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.providers.WeatherContract; + +import java.util.UUID; + +/** + * This class holds the information of a request submitted to the active weather provider service + */ +public final class RequestInfo implements Parcelable { + + private Location mLocation; + private String mCityName; + private WeatherLocation mWeatherLocation; + private int mRequestType; + private IRequestInfoListener mListener; + private int mTempUnit; + private String mKey; + private boolean mIsQueryOnly; + + /** + * A request to update the weather data using a geographical {@link android.location.Location} + */ + public static final int TYPE_WEATHER_BY_GEO_LOCATION_REQ = 1; + /** + * A request to update the weather data using a {@link WeatherLocation} + */ + public static final int TYPE_WEATHER_BY_WEATHER_LOCATION_REQ = 2; + + /** + * A request to look up a city name + */ + public static final int TYPE_LOOKUP_CITY_NAME_REQ = 3; + + private RequestInfo() {} + + /* package */ static class Builder { + private Location mLocation; + private String mCityName; + private WeatherLocation mWeatherLocation; + private int mRequestType; + private IRequestInfoListener mListener; + private int mTempUnit = WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT; + private boolean mIsQueryOnly = false; + + public Builder(IRequestInfoListener listener) { + this.mListener = listener; + } + + /** + * Sets the city name and identifies this request as a {@link #TYPE_LOOKUP_CITY_NAME_REQ} + * request. If set, will null out the location and weather location. Attempting to set + * a null city name will get you an IllegalArgumentException + */ + public Builder setCityName(String cityName) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + this.mCityName = cityName; + this.mRequestType = TYPE_LOOKUP_CITY_NAME_REQ; + this.mLocation = null; + this.mWeatherLocation = null; + return this; + } + + /** + * Sets the Location and identifies this request as a + * {@link #TYPE_WEATHER_BY_GEO_LOCATION_REQ}. If set, will null out the city name and + * weather location. Attempting to set a null location will get you an + * IllegalArgumentException + */ + public Builder setLocation(Location location) { + if (location == null) { + throw new IllegalArgumentException("Location can't be null"); + } + this.mLocation = new Location(location); + this.mCityName = null; + this.mWeatherLocation = null; + this.mRequestType = TYPE_WEATHER_BY_GEO_LOCATION_REQ; + return this; + } + + /** + * Sets the weather location and identifies this request as a + * {@link #TYPE_WEATHER_BY_WEATHER_LOCATION_REQ}. If set, will null out the location and + * city name. Attempting to set a null weather location will get you an + * IllegalArgumentException + */ + public Builder setWeatherLocation(WeatherLocation weatherLocation) { + if (weatherLocation == null) { + throw new IllegalArgumentException("WeatherLocation can't be null"); + } + this.mWeatherLocation = weatherLocation; + this.mLocation = null; + this.mCityName = null; + this.mRequestType = TYPE_WEATHER_BY_WEATHER_LOCATION_REQ; + return this; + } + + /** + * Sets the unit in which the temperature will be reported if the request is honored. + * Valid values are: + *
    + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#CELSIUS} + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#FAHRENHEIT} + *
+ * Any other value will generate an IllegalArgumentException. If the temperature unit is not + * set, the default will be degrees Fahrenheit + * @param unit A valid temperature unit + */ + public Builder setTemperatureUnit(int unit) { + if (!isValidTempUnit(unit)) { + throw new IllegalArgumentException("Invalid temperature unit"); + } + this.mTempUnit = unit; + return this; + } + + /** + * If this is a weather request, marks the request as a query only, meaning that the + * content provider won't be updated after the active weather service has finished + * processing the request. + */ + public Builder queryOnly() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + this.mIsQueryOnly = true; + break; + default: + this.mIsQueryOnly = false; + break; + } + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link RequestInfo} object + * @return {@link RequestInfo} + */ + public RequestInfo build() { + RequestInfo info = new RequestInfo(); + info.mListener = this.mListener; + info.mRequestType = this.mRequestType; + info.mCityName = this.mCityName; + info.mWeatherLocation = this.mWeatherLocation; + info.mLocation = this.mLocation; + info.mTempUnit = this.mTempUnit; + info.mIsQueryOnly = this.mIsQueryOnly; + info.mKey = UUID.randomUUID().toString(); + return info; + } + + private boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } + + } + + private RequestInfo(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mRequestType = parcel.readInt(); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + mLocation = Location.CREATOR.createFromParcel(parcel); + mTempUnit = parcel.readInt(); + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mWeatherLocation = WeatherLocation.CREATOR.createFromParcel(parcel); + mTempUnit = parcel.readInt(); + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + mCityName = parcel.readString(); + break; + } + mIsQueryOnly = (parcel.readInt() == 1); + mListener = IRequestInfoListener.Stub.asInterface(parcel.readStrongBinder()); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + + /** + * @return The request type + */ + public int getRequestType() { + return mRequestType; + } + + /** + * @return the {@link android.location.Location} if this is a request by location, null + * otherwise + */ + public Location getLocation() { + return new Location(mLocation); + } + + /** + * @return the {@link lineageos.weather.WeatherLocation} if this is a request by weather + * location, null otherwise + */ + public WeatherLocation getWeatherLocation() { + return mWeatherLocation; + } + + /** + * @hide + */ + public IRequestInfoListener getRequestListener() { + return mListener; + } + + /** + * @return the city name if this is a lookup request, null otherwise + */ + public String getCityName() { + return mCityName; + } + + /** + * @return the temperature unit if this is a weather request, -1 otherwise + */ + public int getTemperatureUnit() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + return mTempUnit; + default: + return -1; + } + } + + /** + * @return if this is a weather request, whether the request will update the content provider. + * False for other kind of requests + * @hide + */ + public boolean isQueryOnlyWeatherRequest() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + return mIsQueryOnly; + default: + return false; + } + } + + public static final Creator CREATOR = new Creator() { + @Override + public RequestInfo createFromParcel(Parcel in) { + return new RequestInfo(in); + } + + @Override + public RequestInfo[] newArray(int size) { + return new RequestInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeInt(mRequestType); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + mLocation.writeToParcel(dest, 0); + dest.writeInt(mTempUnit); + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mWeatherLocation.writeToParcel(dest, 0); + dest.writeInt(mTempUnit); + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + dest.writeString(mCityName); + break; + } + dest.writeInt(mIsQueryOnly == true ? 1 : 0); + dest.writeStrongBinder(mListener.asBinder()); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("{ Request for "); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + builder.append("Location: ").append(mLocation); + builder.append(" Temp Unit: "); + if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + builder.append("Fahrenheit"); + } else { + builder.append(" Celsius"); + } + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + builder.append("WeatherLocation: ").append(mWeatherLocation); + builder.append(" Temp Unit: "); + if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + builder.append("Fahrenheit"); + } else { + builder.append(" Celsius"); + } + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + builder.append("Lookup City: ").append(mCityName); + break; + } + return builder.append(" }").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + RequestInfo info = (RequestInfo) obj; + return (TextUtils.equals(mKey, info.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weather/WeatherInfo.java b/app/src/main/java/lineageos/weather/WeatherInfo.java new file mode 100755 index 000000000..91d8b3474 --- /dev/null +++ b/app/src/main/java/lineageos/weather/WeatherInfo.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2016 The CyanongenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.providers.WeatherContract; +import lineageos.weatherservice.ServiceRequest; +import lineageos.weatherservice.ServiceRequestResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * This class represents the weather information that a + * {@link lineageos.weatherservice.WeatherProviderService} will use to update the weather content + * provider. A weather provider service will be called by the system to process an update + * request at any time. If the service successfully processes the request, then the weather provider + * service is responsible of calling + * {@link ServiceRequest#complete(ServiceRequestResult)} to notify the + * system that the request was completed and that the weather content provider should be updated + * with the supplied weather information. + */ +public final class WeatherInfo implements Parcelable { + + private String mCity; + private int mConditionCode; + private double mTemperature; + private int mTempUnit; + private double mTodaysHighTemp; + private double mTodaysLowTemp; + private double mHumidity; + private double mWindSpeed; + private double mWindDirection; + private int mWindSpeedUnit; + private long mTimestamp; + private List mForecastList; + private String mKey; + + private WeatherInfo() {} + + /** + * Builder class for {@link WeatherInfo} + */ + public static class Builder { + private String mCity; + private int mConditionCode = WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE; + private double mTemperature; + private int mTempUnit; + private double mTodaysHighTemp = Double.NaN; + private double mTodaysLowTemp = Double.NaN; + private double mHumidity = Double.NaN; + private double mWindSpeed = Double.NaN; + private double mWindDirection = Double.NaN; + private int mWindSpeedUnit = WeatherContract.WeatherColumns.WindSpeedUnit.MPH; + private long mTimestamp = -1; + private List mForecastList = new ArrayList<>(0); + + /** + * @param cityName A valid city name. Attempting to pass null will get you an + * IllegalArgumentException + * @param temperature A valid temperature value. Attempting pass an invalid double value, + * will get you an IllegalArgumentException + * @param tempUnit A valid temperature unit value. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit} for + * valid values. Attempting to pass an invalid temperature unit will get you + * an IllegalArgumentException + */ + public Builder(@NonNull String cityName, double temperature, int tempUnit) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + if (Double.isNaN(temperature)) { + throw new IllegalArgumentException("Invalid temperature"); + } + if (!isValidTempUnit(tempUnit)) { + throw new IllegalArgumentException("Invalid temperature unit"); + } + this.mCity = cityName; + this.mTemperature = temperature; + this.mTempUnit = tempUnit; + } + + /** + * @param timeStamp A timestamp indicating when this data was generated. If timestamps is + * not set, then the builder will set it to the time of object creation + * @return The {@link Builder} instance + */ + public Builder setTimestamp(long timeStamp) { + mTimestamp = timeStamp; + return this; + } + + /** + * @param humidity The weather humidity. Attempting to pass an invalid double value will get + * you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setHumidity(double humidity) { + if (Double.isNaN(humidity)) { + throw new IllegalArgumentException("Invalid humidity value"); + } + + mHumidity = humidity; + return this; + } + + /** + * @param windSpeed The wind speed. Attempting to pass an invalid double value will get you + * an IllegalArgumentException + * @param windDirection The wind direction. Attempting to pass an invalid double value will + * get you an IllegalArgumentException + * @param windSpeedUnit A valid wind speed direction unit. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit} + * for valid values. Attempting to pass an invalid speed unit will get + * you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setWind(double windSpeed, double windDirection, int windSpeedUnit) { + if (Double.isNaN(windSpeed)) { + throw new IllegalArgumentException("Invalid wind speed value"); + } + if (Double.isNaN(windDirection)) { + throw new IllegalArgumentException("Invalid wind direction value"); + } + if (!isValidWindSpeedUnit(windSpeedUnit)) { + throw new IllegalArgumentException("Invalid speed unit"); + } + mWindSpeed = windSpeed; + mWindSpeedUnit = windSpeedUnit; + mWindDirection = windDirection; + return this; + } + + /** + * @param conditionCode A valid weather condition code. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode} + * for valid codes. Attempting to pass an invalid code will get you an + * IllegalArgumentException. + * @return The {@link Builder} instance + */ + public Builder setWeatherCondition(int conditionCode) { + if (!isValidWeatherCode(conditionCode)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = conditionCode; + return this; + } + + /** + * @param forecasts A valid array list of {@link DayForecast} objects. Attempting to pass + * null will get you an IllegalArgumentException' + * @return The {@link Builder} instance + */ + public Builder setForecast(@NonNull List forecasts) { + if (forecasts == null) { + throw new IllegalArgumentException("Forecast list can't be null"); + } + mForecastList = forecasts; + return this; + } + + /** + * + * @param todaysHigh Today's high temperature. Attempting to pass an invalid double value + * will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setTodaysHigh(double todaysHigh) { + if (Double.isNaN(todaysHigh)) { + throw new IllegalArgumentException("Invalid temperature value"); + } + mTodaysHighTemp = todaysHigh; + return this; + } + + /** + * @param todaysLow Today's low temperature. Attempting to pass an invalid double value will + * get you an IllegalArgumentException + * @return + */ + public Builder setTodaysLow(double todaysLow) { + if (Double.isNaN(todaysLow)) { + throw new IllegalArgumentException("Invalid temperature value"); + } + mTodaysLowTemp = todaysLow; + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link WeatherInfo} object + * @return {@link WeatherInfo} + */ + public WeatherInfo build() { + WeatherInfo info = new WeatherInfo(); + info.mCity = this.mCity; + info.mConditionCode = this.mConditionCode; + info.mTemperature = this.mTemperature; + info.mTempUnit = this.mTempUnit; + info.mHumidity = this.mHumidity; + info.mWindSpeed = this.mWindSpeed; + info.mWindDirection = this.mWindDirection; + info.mWindSpeedUnit = this.mWindSpeedUnit; + info.mTimestamp = this.mTimestamp == -1 ? System.currentTimeMillis() : this.mTimestamp; + info.mForecastList = this.mForecastList; + info.mTodaysHighTemp = this.mTodaysHighTemp; + info.mTodaysLowTemp = this.mTodaysLowTemp; + info.mKey = UUID.randomUUID().toString(); + return info; + } + + private boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } + + private boolean isValidWindSpeedUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.WindSpeedUnit.KPH: + case WeatherContract.WeatherColumns.WindSpeedUnit.MPH: + return true; + default: + return false; + } + } + } + + + private static boolean isValidWeatherCode(int code) { + if (code < WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MIN + || code > WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MAX) { + if (code != WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE) { + return false; + } + } + return true; + } + + /** + * @return city name + */ + public String getCity() { + return mCity; + } + + /** + * @return An implementation specific weather condition code + */ + public int getConditionCode() { + return mConditionCode; + } + + /** + * @return humidity + */ + public double getHumidity() { + return mHumidity; + } + + /** + * @return time stamp when the request was processed + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * @return wind direction (degrees) + */ + public double getWindDirection() { + return mWindDirection; + } + + /** + * @return wind speed + */ + public double getWindSpeed() { + return mWindSpeed; + } + + /** + * @return wind speed unit + */ + public int getWindSpeedUnit() { + return mWindSpeedUnit; + } + + /** + * @return current temperature + */ + public double getTemperature() { + return mTemperature; + } + + /** + * @return temperature unit + */ + public int getTemperatureUnit() { + return mTempUnit; + } + + /** + * @return today's high temperature + */ + public double getTodaysHigh() { + return mTodaysHighTemp; + } + + /** + * @return today's low temperature + */ + public double getTodaysLow() { + return mTodaysLowTemp; + } + + /** + * @return List of {@link lineageos.weather.WeatherInfo.DayForecast}. This list will contain + * the forecast weather for the upcoming days. If you want to know today's high and low + * temperatures, use {@link WeatherInfo#getTodaysHigh()} and {@link WeatherInfo#getTodaysLow()} + */ + public List getForecasts() { + return new ArrayList<>(mForecastList); + } + + private WeatherInfo(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mCity = parcel.readString(); + mConditionCode = parcel.readInt(); + mTemperature = parcel.readDouble(); + mTempUnit = parcel.readInt(); + mHumidity = parcel.readDouble(); + mWindSpeed = parcel.readDouble(); + mWindDirection = parcel.readDouble(); + mWindSpeedUnit = parcel.readInt(); + mTodaysHighTemp = parcel.readDouble(); + mTodaysLowTemp = parcel.readDouble(); + mTimestamp = parcel.readLong(); + int forecastListSize = parcel.readInt(); + mForecastList = new ArrayList<>(); + while (forecastListSize > 0) { + mForecastList.add(DayForecast.CREATOR.createFromParcel(parcel)); + forecastListSize--; + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeString(mCity); + dest.writeInt(mConditionCode); + dest.writeDouble(mTemperature); + dest.writeInt(mTempUnit); + dest.writeDouble(mHumidity); + dest.writeDouble(mWindSpeed); + dest.writeDouble(mWindDirection); + dest.writeInt(mWindSpeedUnit); + dest.writeDouble(mTodaysHighTemp); + dest.writeDouble(mTodaysLowTemp); + dest.writeLong(mTimestamp); + dest.writeInt(mForecastList.size()); + for (DayForecast dayForecast : mForecastList) { + dayForecast.writeToParcel(dest, 0); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public WeatherInfo createFromParcel(Parcel source) { + return new WeatherInfo(source); + } + + @Override + public WeatherInfo[] newArray(int size) { + return new WeatherInfo[size]; + } + }; + + /** + * This class represents the weather forecast for a given day. Do not add low and high + * temperatures for the current day in this list. Use + * {@link WeatherInfo.Builder#setTodaysHigh(double)} and + * {@link WeatherInfo.Builder#setTodaysLow(double)} instead. + */ + public static class DayForecast implements Parcelable{ + double mLow; + double mHigh; + int mConditionCode; + String mKey; + + private DayForecast() {} + + /** + * Builder class for {@link DayForecast} + */ + public static class Builder { + double mLow = Double.NaN; + double mHigh = Double.NaN; + int mConditionCode; + + /** + * @param conditionCode A valid weather condition code. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode} for valid + * values. Attempting to pass an invalid code will get you an + * IllegalArgumentException + */ + public Builder(int conditionCode) { + if (!isValidWeatherCode(conditionCode)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = conditionCode; + } + + /** + * @param high Forecast high temperature for this day. Attempting to pass an invalid + * double value will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setHigh(double high) { + if (Double.isNaN(high)) { + throw new IllegalArgumentException("Invalid high forecast temperature"); + } + mHigh = high; + return this; + } + + /** + * @param low Forecast low temperate for this day. Attempting to pass an invalid double + * value will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setLow(double low) { + if (Double.isNaN(low)) { + throw new IllegalArgumentException("Invalid low forecast temperature"); + } + mLow = low; + return this; + } + + + /** + * Combine all of the options that have been set and return a new {@link DayForecast} + * object + * @return {@link DayForecast} + */ + public DayForecast build() { + DayForecast forecast = new DayForecast(); + forecast.mLow = this.mLow; + forecast.mHigh = this.mHigh; + forecast.mConditionCode = this.mConditionCode; + forecast.mKey = UUID.randomUUID().toString(); + return forecast; + } + } + + /** + * @return forecasted low temperature + */ + public double getLow() { + return mLow; + } + + /** + * @return not what you think. Returns the forecasted high temperature + */ + public double getHigh() { + return mHigh; + } + + /** + * @return forecasted weather condition code. Implementation specific + */ + public int getConditionCode() { + return mConditionCode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeDouble(mLow); + dest.writeDouble(mHigh); + dest.writeInt(mConditionCode); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public DayForecast createFromParcel(Parcel source) { + return new DayForecast(source); + } + + @Override + public DayForecast[] newArray(int size) { + return new DayForecast[size]; + } + }; + + private DayForecast(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mLow = parcel.readDouble(); + mHigh = parcel.readDouble(); + mConditionCode = parcel.readInt(); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{Low temp: ").append(mLow) + .append(" High temp: ").append(mHigh) + .append(" Condition code: ").append(mConditionCode) + .append("}").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + DayForecast forecast = (DayForecast) obj; + return (TextUtils.equals(mKey, forecast.mKey)); + } + return false; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder() + .append(" City Name: ").append(mCity) + .append(" Condition Code: ").append(mConditionCode) + .append(" Temperature: ").append(mTemperature) + .append(" Temperature Unit: ").append(mTempUnit) + .append(" Humidity: ").append(mHumidity) + .append(" Wind speed: ").append(mWindSpeed) + .append(" Wind direction: ").append(mWindDirection) + .append(" Wind Speed Unit: ").append(mWindSpeedUnit) + .append(" Today's high temp: ").append(mTodaysHighTemp) + .append(" Today's low temp: ").append(mTodaysLowTemp) + .append(" Timestamp: ").append(mTimestamp).append(" Forecasts: ["); + for (DayForecast dayForecast : mForecastList) { + builder.append(dayForecast.toString()); + } + return builder.append("]}").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + WeatherInfo info = (WeatherInfo) obj; + return (TextUtils.equals(mKey, info.mKey)); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/lineageos/weather/WeatherLocation.java b/app/src/main/java/lineageos/weather/WeatherLocation.java new file mode 100644 index 000000000..21a6b07ba --- /dev/null +++ b/app/src/main/java/lineageos/weather/WeatherLocation.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; + +import java.util.UUID; + +/** + * A class representing a geographical location that a weather service provider can use to + * get weather data from. Each service provider will potentially populate objects of this class + * with different content, so make sure you don't preserve the values when a service provider + * is changed + */ +public final class WeatherLocation implements Parcelable{ + private String mCityId; + private String mCity; + private String mState; + private String mPostal; + private String mCountryId; + private String mCountry; + private String mKey; + + private WeatherLocation() {} + + /** + * Builder class for {@link WeatherLocation} + */ + public static class Builder { + String mCityId = ""; + String mCity = ""; + String mState = ""; + String mPostal = ""; + String mCountryId = ""; + String mCountry = ""; + + /** + * @param cityId An identifier for the city (for example WOEID - Where On Earth IDentifier) + * @param cityName The name of the city + */ + public Builder(String cityId, String cityName) { + if (cityId == null || cityName == null) { + throw new IllegalArgumentException("Illegal to set city id AND city to null"); + } + this.mCityId = cityId; + this.mCity = cityName; + } + + /** + * @param cityName The name of the city + */ + public Builder(String cityName) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + this.mCity = cityName; + } + + /** + * @param countryId An identifier for the country (for example ISO alpha-2, ISO alpha-3, + * ISO 3166-1 numeric-3, etc) + * @return The {@link Builder} instance + */ + public Builder setCountryId(String countryId) { + if (countryId == null) { + throw new IllegalArgumentException("Country ID can't be null"); + } + this.mCountryId = countryId; + return this; + } + + /** + * @param country The country name + * @return The {@link Builder} instance + */ + public Builder setCountry(String country) { + if (country == null) { + throw new IllegalArgumentException("Country can't be null"); + } + this.mCountry = country; + return this; + } + + /** + * @param postalCode The postal/ZIP code + * @return The {@link Builder} instance + */ + public Builder setPostalCode(String postalCode) { + if (postalCode == null) { + throw new IllegalArgumentException("Postal code/ZIP can't be null"); + } + this.mPostal = postalCode; + return this; + } + + /** + * @param state The state or territory where the city is located + * @return The {@link Builder} instance + */ + public Builder setState(String state) { + if (state == null) { + throw new IllegalArgumentException("State can't be null"); + } + this.mState = state; + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link WeatherLocation} + * object + * @return {@link WeatherLocation} + */ + public WeatherLocation build() { + WeatherLocation weatherLocation = new WeatherLocation(); + weatherLocation.mCityId = this.mCityId; + weatherLocation.mCity = this.mCity; + weatherLocation.mState = this.mState; + weatherLocation.mPostal = this.mPostal; + weatherLocation.mCountryId = this.mCountryId; + weatherLocation.mCountry = this.mCountry; + weatherLocation.mKey = UUID.randomUUID().toString(); + return weatherLocation; + } + } + + /** + * @return The city ID. This method will return an empty string if the city ID was not set + */ + public String getCityId() { + return mCityId; + } + + /** + * @return The city name. This method will return an empty string if the city name was not set + */ + public String getCity() { + return mCity; + } + + /** + * @return The state name. This method will return an empty string if the state was not set + */ + public String getState() { + return mState; + } + + /** + * @return The postal/ZIP code. This method will return an empty string if the postal/ZIP code + * was not set + */ + public String getPostalCode() { + return mPostal; + } + + /** + * @return The country ID. This method will return an empty string if the country ID was not set + */ + public String getCountryId() { + return mCountryId; + } + + /** + * @return The country name. This method will return an empty string if the country ID was not + * set + */ + public String getCountry() { + return mCountry; + } + + private WeatherLocation(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = in.readString(); + mCityId = in.readString(); + mCity = in.readString(); + mState = in.readString(); + mPostal = in.readString(); + mCountryId = in.readString(); + mCountry = in.readString(); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public WeatherLocation createFromParcel(Parcel in) { + return new WeatherLocation(in); + } + + @Override + public WeatherLocation[] newArray(int size) { + return new WeatherLocation[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeString(mCityId); + dest.writeString(mCity); + dest.writeString(mState); + dest.writeString(mPostal); + dest.writeString(mCountryId); + dest.writeString(mCountry); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{ City ID: ").append(mCityId) + .append(" City: ").append(mCity) + .append(" State: ").append(mState) + .append(" Postal/ZIP Code: ").append(mPostal) + .append(" Country Id: ").append(mCountryId) + .append(" Country: ").append(mCountry).append("}") + .toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + WeatherLocation location = (WeatherLocation) obj; + return (TextUtils.equals(mKey, location.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weather/util/WeatherUtils.java b/app/src/main/java/lineageos/weather/util/WeatherUtils.java new file mode 100644 index 000000000..dd8418ffd --- /dev/null +++ b/app/src/main/java/lineageos/weather/util/WeatherUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather.util; + + +import lineageos.providers.WeatherContract; + +import java.text.DecimalFormat; + +/** + * Helper class to perform operations and formatting of weather data + */ +public class WeatherUtils { + + /** + * Converts a temperature expressed in degrees Celsius to degrees Fahrenheit + * @param celsius temperature in Celsius + * @return the temperature in degrees Fahrenheit + */ + public static double celsiusToFahrenheit(double celsius) { + return ((celsius * (9d/5d)) + 32d); + } + + /** + * Converts a temperature expressed in degrees Fahrenheit to degrees Celsius + * @param fahrenheit temperature in Fahrenheit + * @return the temperature in degrees Celsius + */ + public static double fahrenheitToCelsius(double fahrenheit) { + return ((fahrenheit - 32d) * (5d/9d)); + } + + /** + * Returns a string representation of the temperature and unit supplied. The temperature value + * will be half-even rounded. + * @param temperature the temperature value + * @param tempUnit A valid {@link WeatherContract.WeatherColumns.TempUnit} + * @return A string with the format XX°F or XX°C (where XX is the temperature) + * depending on the temperature unit that was provided or null if an invalid unit is supplied + */ + public static String formatTemperature(double temperature, int tempUnit) { + if (!isValidTempUnit(tempUnit)) return null; + if (Double.isNaN(temperature)) return "-"; + + DecimalFormat noDigitsFormat = new DecimalFormat("0"); + String noDigitsTemp = noDigitsFormat.format(temperature); + if (noDigitsTemp.equals("-0")) { + noDigitsTemp = "0"; + } + + StringBuilder formatted = new StringBuilder() + .append(noDigitsTemp).append("\u00b0"); + if (tempUnit == WeatherContract.WeatherColumns.TempUnit.CELSIUS) { + formatted.append("C"); + } else if (tempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + formatted.append("F"); + } + return formatted.toString(); + } + + private static boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } +} diff --git a/app/src/main/java/lineageos/weatherservice/ServiceRequest.java b/app/src/main/java/lineageos/weatherservice/ServiceRequest.java new file mode 100644 index 000000000..7f45e5686 --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/ServiceRequest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + + +import android.os.RemoteException; + +import androidx.annotation.NonNull; + +import lineageos.weatherservice.IWeatherProviderServiceClient; +import lineageos.weather.LineageWeatherManager; +import lineageos.weather.RequestInfo; + +/** + * This class represents a request submitted by the system to the active weather provider service + */ +public final class ServiceRequest { + + private final RequestInfo mInfo; + private final IWeatherProviderServiceClient mClient; + + private enum Status { + IN_PROGRESS, COMPLETED, CANCELLED, FAILED, REJECTED + } + private Status mStatus; + + /* package */ ServiceRequest(RequestInfo info, IWeatherProviderServiceClient client) { + mInfo = info; + mClient = client; + mStatus = Status.IN_PROGRESS; + } + + /** + * Obtains the request information + * @return {@link lineageos.weather.RequestInfo} + */ + public RequestInfo getRequestInfo() { + return mInfo; + } + + /** + * This method should be called once the request has been completed + */ + public void complete(@NonNull ServiceRequestResult result) { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + if (result.getWeatherInfo() == null) { + throw new IllegalStateException("The service request result doesn't" + + " contain a valid WeatherInfo object"); + } + mClient.setServiceRequestState(mInfo, result, + LineageWeatherManager.RequestStatus.COMPLETED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + if (result.getLocationLookupList() == null + || result.getLocationLookupList().size() <= 0) { + //In case the user decided to mark this request as completed with + //null or empty list. It's not necessarily a failure + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.NO_MATCH_FOUND); + } else { + mClient.setServiceRequestState(mInfo, result, + LineageWeatherManager.RequestStatus.COMPLETED); + } + break; + } + } catch (RemoteException e) { + } + mStatus = Status.COMPLETED; + } + } + } + + /** + * This method should be called if the service failed to process the request + * (no internet connection, time out, etc.) + */ + public void fail() { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.FAILED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.FAILED); + break; + } + } catch (RemoteException e) { + } + mStatus = Status.FAILED; + } + } + } + + /** + * This method should be called if the service decides not to honor the request. Note this + * method will accept only the following values. + *
    + *
  • {@link lineageos.weather.LineageWeatherManager.RequestStatus#SUBMITTED_TOO_SOON}
  • + *
  • {@link lineageos.weather.LineageWeatherManager.RequestStatus#ALREADY_IN_PROGRESS}
  • + *
+ * Attempting to pass any other value will get you an IllegalArgumentException + * @param status + */ + public void reject(int status) { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + switch (status) { + case LineageWeatherManager.RequestStatus.ALREADY_IN_PROGRESS: + case LineageWeatherManager.RequestStatus.SUBMITTED_TOO_SOON: + try { + mClient.setServiceRequestState(mInfo, null, status); + } catch (RemoteException e) { + e.printStackTrace(); + } + break; + default: + throw new IllegalArgumentException("Can't reject with status " + status); + } + mStatus = Status.REJECTED; + } + } + } + + /** + * Called by the WeatherProviderService base class to notify we don't want this request anymore. + * The service implementing the WeatherProviderService will be notified of this action + * via onRequestCancelled() + * @hide + */ + public void cancel() { + synchronized (this) { + mStatus = Status.CANCELLED; + } + } +} diff --git a/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java b/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java new file mode 100644 index 000000000..cb1b2a0fc --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.weather.WeatherLocation; +import lineageos.weather.WeatherInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Use this class to build a request result. + */ +public final class ServiceRequestResult implements Parcelable { + + private WeatherInfo mWeatherInfo; + private List mLocationLookupList; + private String mKey; + + private ServiceRequestResult() {} + + private ServiceRequestResult(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = in.readString(); + int hasWeatherInfo = in.readInt(); + if (hasWeatherInfo == 1) { + mWeatherInfo = WeatherInfo.CREATOR.createFromParcel(in); + } + int hasLocationLookupList = in.readInt(); + if (hasLocationLookupList == 1) { + mLocationLookupList = new ArrayList<>(); + int listSize = in.readInt(); + while (listSize > 0) { + mLocationLookupList.add(WeatherLocation.CREATOR.createFromParcel(in)); + listSize--; + } + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Creator CREATOR + = new Creator() { + @Override + public ServiceRequestResult createFromParcel(Parcel in) { + return new ServiceRequestResult(in); + } + + @Override + public ServiceRequestResult[] newArray(int size) { + return new ServiceRequestResult[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + if (mWeatherInfo != null) { + dest.writeInt(1); + mWeatherInfo.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mLocationLookupList != null) { + dest.writeInt(1); + dest.writeInt(mLocationLookupList.size()); + for (WeatherLocation lookup : mLocationLookupList) { + lookup.writeToParcel(dest, 0); + } + } else { + dest.writeInt(0); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + /** + * Builder class for {@link ServiceRequestResult} + */ + public static class Builder { + private WeatherInfo mWeatherInfo; + private List mLocationLookupList; + public Builder() { + this.mWeatherInfo = null; + this.mLocationLookupList = null; + } + + /** + * @param weatherInfo The WeatherInfo object holding the data that will be used to update + * the weather content provider + */ + public Builder(@NonNull WeatherInfo weatherInfo) { + if (weatherInfo == null) { + throw new IllegalArgumentException("WeatherInfo can't be null"); + } + + mWeatherInfo = weatherInfo; + } + + /** + * @param locations The list of WeatherLocation objects. The list should not be null + */ + public Builder(@NonNull List locations) { + if (locations == null) { + throw new IllegalArgumentException("Weather location list can't be null"); + } + mLocationLookupList = locations; + } + + /** + * Creates a {@link ServiceRequestResult} with the arguments + * supplied to this builder + * @return {@link ServiceRequestResult} + */ + public ServiceRequestResult build() { + ServiceRequestResult result = new ServiceRequestResult(); + result.mWeatherInfo = this.mWeatherInfo; + result.mLocationLookupList = this.mLocationLookupList; + result.mKey = UUID.randomUUID().toString(); + return result; + } + } + + /** + * @return The WeatherInfo object supplied by the weather provider service + */ + public WeatherInfo getWeatherInfo() { + return mWeatherInfo; + } + + /** + * @return The list of WeatherLocation objects supplied by the weather provider service + */ + public List getLocationLookupList() { + return new ArrayList<>(mLocationLookupList); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + ServiceRequestResult request = (ServiceRequestResult) obj; + return (TextUtils.equals(mKey, request.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java b/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java new file mode 100644 index 000000000..2e3a53539 --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import lineageos.weather.RequestInfo; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + + +public abstract class WeatherProviderService extends Service { + + private Handler mHandler; + private IWeatherProviderServiceClient mClient; + private Set mWeakRequestsSet + = Collections.newSetFromMap(new WeakHashMap()); + + /** + * The {@link android.content.Intent} action that must be declared as handled by a service in + * its manifest for the system to recognize it as a weather provider service + */ + public static final String SERVICE_INTERFACE + = "lineageos.weatherservice.WeatherProviderService"; + + /** + * Name under which a {@link WeatherProviderService} publishes information about itself. + * This meta-data must reference an XML resource containing + * a <weather-provider-service> + * tag. + */ + public static final String SERVICE_META_DATA = "lineageos.weatherservice"; + + @Override + protected final void attachBaseContext(Context base) { + super.attachBaseContext(base); + mHandler = new ServiceHandler(base.getMainLooper()); + } + + @Override + public final IBinder onBind(Intent intent) { + return mBinder; + } + + private final IWeatherProviderService.Stub mBinder = new IWeatherProviderService.Stub() { + + @Override + public void processWeatherUpdateRequest(final RequestInfo info) { + mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget(); + } + + @Override + public void processCityNameLookupRequest(final RequestInfo info) { + mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget(); + } + + @Override + public void setServiceClient(IWeatherProviderServiceClient client) { + mHandler.obtainMessage(ServiceHandler.MSG_SET_CLIENT, client).sendToTarget(); + } + + @Override + public void cancelOngoingRequests() { + synchronized (mWeakRequestsSet) { + for (final ServiceRequest request : mWeakRequestsSet) { + request.cancel(); + mWeakRequestsSet.remove(request); + mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request) + .sendToTarget(); + } + } + } + + @Override + public void cancelRequest(int requestId) { + synchronized (mWeakRequestsSet) { + for (final ServiceRequest request : mWeakRequestsSet) { + if (request.getRequestInfo().hashCode() == requestId) { + mWeakRequestsSet.remove(request); + request.cancel(); + mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request) + .sendToTarget(); + break; + } + } + } + } + }; + + private class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + public static final int MSG_SET_CLIENT = 1; + public static final int MSG_ON_NEW_REQUEST = 2; + public static final int MSG_CANCEL_REQUEST = 3; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_CLIENT: { + mClient = (IWeatherProviderServiceClient) msg.obj; + if (mClient != null) { + onConnected(); + } else { + onDisconnected(); + } + return; + } + case MSG_ON_NEW_REQUEST: { + RequestInfo info = (RequestInfo) msg.obj; + if (info != null) { + ServiceRequest request = new ServiceRequest(info, mClient); + synchronized (mWeakRequestsSet) { + mWeakRequestsSet.add(request); + } + onRequestSubmitted(request); + } + return; + } + case MSG_CANCEL_REQUEST: { + ServiceRequest request = (ServiceRequest) msg.obj; + onRequestCancelled(request); + return; + } + } + } + } + + /** + * The system has connected to this service. + */ + protected void onConnected() { + /* Do nothing */ + } + + /** + * The system has disconnected from this service. + */ + protected void onDisconnected() { + /* Do nothing */ + } + + /** + * A new request has been submitted to this service + * @param request The service request to be processed by this service + */ + protected abstract void onRequestSubmitted(ServiceRequest request); + + /** + * Called when the system is not interested on this request anymore. Note that the service + * has marked the request as cancelled and you must stop any ongoing operation + * (such as pulling data from internet) that this service could've been performing to honor the + * request. + * + * @param request The request cancelled by the system + */ + protected abstract void onRequestCancelled(ServiceRequest request); +} \ No newline at end of file From a70aa5e749cb7f876f208be330be3db55580fb40 Mon Sep 17 00:00:00 2001 From: keeshii Date: Thu, 22 Aug 2019 17:57:26 +0200 Subject: [PATCH 005/154] Added LineageOs Weather receiver. --- app/src/main/AndroidManifest.xml | 2 + .../LineageOsWeatherReceiver.java | 221 ++++++++++++++++++ .../service/DeviceCommunicationService.java | 10 + 3 files changed, 233 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/LineageOsWeatherReceiver.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bfd292b9..6e962b68c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,8 @@ + + . */ +package nodomain.freeyourgadget.gadgetbridge.externalevents; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import lineageos.weather.LineageWeatherManager; +import lineageos.weather.WeatherInfo; +import lineageos.weather.WeatherLocation; +import lineageos.weather.util.WeatherUtils; +import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import static lineageos.providers.WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT; +import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.ISOLATED_THUNDERSHOWERS; +import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE; +import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_SNOW_SHOWERS; +import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_THUNDERSTORMS; +import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SHOWERS; +import static lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit.MPH; + +public class LineageOsWeatherReceiver extends BroadcastReceiver implements LineageWeatherManager.WeatherUpdateRequestListener, LineageWeatherManager.LookupCityRequestListener { + + private static final Logger LOG = LoggerFactory.getLogger(LineageOsWeatherReceiver.class); + + private WeatherLocation weatherLocation = null; + private Context mContext; + private PendingIntent mPendingIntent = null; + + public LineageOsWeatherReceiver() { + mContext = GBApplication.getContext(); + final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(mContext); + if (weatherManager == null) { + return; + } + + Prefs prefs = GBApplication.getPrefs(); + + String city = prefs.getString("weather_city", null); + String cityId = prefs.getString("weather_cityid", null); + + if ((cityId == null || cityId.equals("")) && city != null && !city.equals("")) { + lookupCity(city); + } else if (city != null && cityId != null) { + weatherLocation = new WeatherLocation.Builder(cityId, city).build(); + enablePeriodicAlarm(true); + } + } + + private void lookupCity(String city) { + final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(mContext); + if (weatherManager == null) { + return; + } + + if (city != null && !city.equals("")) { + if (weatherManager.getActiveWeatherServiceProviderLabel() != null) { + weatherManager.lookupCity(city, this); + } + } + } + + private void enablePeriodicAlarm(boolean enable) { + if ((mPendingIntent != null && enable) || (mPendingIntent == null && !enable)) { + return; + } + + AlarmManager am = (AlarmManager) (mContext.getSystemService(Context.ALARM_SERVICE)); + if (am == null) { + LOG.warn("could not get alarm manager!"); + return; + } + + if (enable) { + Intent intent = new Intent("GB_UPDATE_WEATHER"); + intent.setPackage(BuildConfig.APPLICATION_ID); + mPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + am.setInexactRepeating(AlarmManager.RTC_WAKEUP, Calendar.getInstance().getTimeInMillis() + 10000, AlarmManager.INTERVAL_HOUR, mPendingIntent); + } else { + am.cancel(mPendingIntent); + mPendingIntent = null; + } + } + + @Override + public void onReceive(Context context, Intent intent) { + Prefs prefs = GBApplication.getPrefs(); + + String city = prefs.getString("weather_city", null); + String cityId = prefs.getString("weather_cityid", null); + + if (city != null && !city.equals("") && cityId == null) { + lookupCity(city); + } else { + requestWeather(); + } + } + + private void requestWeather() { + final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(GBApplication.getContext()); + if (weatherManager.getActiveWeatherServiceProviderLabel() != null && weatherLocation != null) { + weatherManager.requestWeatherUpdate(weatherLocation, this); + } + } + + @Override + public void onWeatherRequestCompleted(int status, WeatherInfo weatherInfo) { + if (weatherInfo != null) { + LOG.info("weather: " + weatherInfo.toString()); + WeatherSpec weatherSpec = new WeatherSpec(); + weatherSpec.timestamp = (int) (weatherInfo.getTimestamp() / 1000); + weatherSpec.location = weatherInfo.getCity(); + + if (weatherInfo.getTemperatureUnit() == FAHRENHEIT) { + weatherSpec.currentTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTemperature()) + 273; + weatherSpec.todayMaxTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTodaysHigh()) + 273; + weatherSpec.todayMinTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTodaysLow()) + 273; + } else { + weatherSpec.currentTemp = (int) weatherInfo.getTemperature() + 273; + weatherSpec.todayMaxTemp = (int) weatherInfo.getTodaysHigh() + 273; + weatherSpec.todayMinTemp = (int) weatherInfo.getTodaysLow() + 273; + } + if (weatherInfo.getWindSpeedUnit() == MPH) { + weatherSpec.windSpeed = (float) weatherInfo.getWindSpeed() * 1.609344f; + } else { + weatherSpec.windSpeed = (float) weatherInfo.getWindSpeed(); + } + weatherSpec.windDirection = (int) weatherInfo.getWindDirection(); + + weatherSpec.currentConditionCode = Weather.mapToOpenWeatherMapCondition(LineageOstoYahooCondintion(weatherInfo.getConditionCode())); + weatherSpec.currentCondition = Weather.getConditionString(weatherSpec.currentConditionCode); + weatherSpec.currentHumidity = (int) weatherInfo.getHumidity(); + + weatherSpec.forecasts = new ArrayList<>(); + List forecasts = weatherInfo.getForecasts(); + for (int i = 1; i < forecasts.size(); i++) { + WeatherInfo.DayForecast cmForecast = forecasts.get(i); + WeatherSpec.Forecast gbForecast = new WeatherSpec.Forecast(); + if (weatherInfo.getTemperatureUnit() == FAHRENHEIT) { + gbForecast.maxTemp = (int) WeatherUtils.fahrenheitToCelsius(cmForecast.getHigh()) + 273; + gbForecast.minTemp = (int) WeatherUtils.fahrenheitToCelsius(cmForecast.getLow()) + 273; + } else { + gbForecast.maxTemp = (int) cmForecast.getHigh() + 273; + gbForecast.minTemp = (int) cmForecast.getLow() + 273; + } + gbForecast.conditionCode = Weather.mapToOpenWeatherMapCondition(LineageOstoYahooCondintion(cmForecast.getConditionCode())); + weatherSpec.forecasts.add(gbForecast); + } + Weather.getInstance().setWeatherSpec(weatherSpec); + GBApplication.deviceService().onSendWeather(weatherSpec); + } else { + LOG.info("request has returned null for WeatherInfo"); + } + } + + /** + * @param cmCondition + * @return + */ + private int LineageOstoYahooCondintion(int cmCondition) { + int yahooCondition; + if (cmCondition <= SHOWERS) { + yahooCondition = cmCondition; + } else if (cmCondition <= SCATTERED_THUNDERSTORMS) { + yahooCondition = cmCondition + 1; + } else if (cmCondition <= SCATTERED_SNOW_SHOWERS) { + yahooCondition = cmCondition + 2; + } else if (cmCondition <= ISOLATED_THUNDERSHOWERS) { + yahooCondition = cmCondition + 3; + } else { + yahooCondition = NOT_AVAILABLE; + } + return yahooCondition; + } + + @Override + public void onLookupCityRequestCompleted(int result, List list) { + if (list != null) { + weatherLocation = list.get(0); + String cityId = weatherLocation.getCityId(); + String city = weatherLocation.getCity(); + + SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); + editor.putString("weather_city", city).apply(); + editor.putString("weather_cityid", cityId).apply(); + enablePeriodicAlarm(true); + requestWeather(); + } else { + enablePeriodicAlarm(false); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 1a7876332..2ea7f740a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectRecei import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothPairingRequestReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.CMWeatherReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver; +import nodomain.freeyourgadget.gadgetbridge.externalevents.LineageOsWeatherReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.MusicPlaybackReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.OmniJawsObserver; import nodomain.freeyourgadget.gadgetbridge.externalevents.PebbleReceiver; @@ -193,6 +194,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere private AlarmReceiver mAlarmReceiver = null; private CalendarReceiver mCalendarReceiver = null; private CMWeatherReceiver mCMWeatherReceiver = null; + private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null; private OmniJawsObserver mOmniJawsObserver = null; private Random mRandom = new Random(); @@ -733,6 +735,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mCMWeatherReceiver = new CMWeatherReceiver(); registerReceiver(mCMWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER")); } + if (mLineageOsWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) { + mLineageOsWeatherReceiver = new LineageOsWeatherReceiver(); + registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER")); + } if (mOmniJawsObserver == null && coordinator != null && coordinator.supportsWeather()) { try { mOmniJawsObserver = new OmniJawsObserver(new Handler()); @@ -784,6 +790,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere unregisterReceiver(mCMWeatherReceiver); mCMWeatherReceiver = null; } + if (mLineageOsWeatherReceiver != null) { + unregisterReceiver(mLineageOsWeatherReceiver); + mLineageOsWeatherReceiver = null; + } if (mOmniJawsObserver != null) { getContentResolver().unregisterContentObserver(mOmniJawsObserver); } From 8bed673a9591a162ef398996fbd5d14b018e7f74 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 22 Aug 2019 21:31:08 +0200 Subject: [PATCH 006/154] Annotate LineageOsWeatherReceiver as Android >=M, and only try to use it with Oreo or later --- .../LineageOsWeatherReceiver.java | 4 ++++ .../service/DeviceCommunicationService.java | 18 +++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/LineageOsWeatherReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/LineageOsWeatherReceiver.java index e1deb346a..b42bde56b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/LineageOsWeatherReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/LineageOsWeatherReceiver.java @@ -22,6 +22,9 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.os.Build; + +import androidx.annotation.RequiresApi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,6 +51,7 @@ import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SCA import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SHOWERS; import static lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit.MPH; +@RequiresApi(api = Build.VERSION_CODES.M) public class LineageOsWeatherReceiver extends BroadcastReceiver implements LineageWeatherManager.WeatherUpdateRequestListener, LineageWeatherManager.LookupCityRequestListener { private static final Logger LOG = LoggerFactory.getLogger(LineageOsWeatherReceiver.class); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 2ea7f740a..152d457d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -36,6 +36,10 @@ import android.os.Handler; import android.os.IBinder; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,9 +47,6 @@ import java.util.ArrayList; import java.util.Random; import java.util.UUID; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; @@ -97,12 +98,12 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FI import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_HEARTRATE_TEST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RESET; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION; -import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_WEATHER; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO; @@ -735,9 +736,12 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mCMWeatherReceiver = new CMWeatherReceiver(); registerReceiver(mCMWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER")); } - if (mLineageOsWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) { - mLineageOsWeatherReceiver = new LineageOsWeatherReceiver(); - registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER")); + if (GBApplication.isRunningOreoOrLater()) { + if (mLineageOsWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) { + + mLineageOsWeatherReceiver = new LineageOsWeatherReceiver(); + registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER")); + } } if (mOmniJawsObserver == null && coordinator != null && coordinator.supportsWeather()) { try { From 2d233141b421d13cd1cf9412794280525bb081b3 Mon Sep 17 00:00:00 2001 From: vanous Date: Sat, 24 Aug 2019 11:58:32 +0200 Subject: [PATCH 007/154] adds custom renderer for better view of many columns --- .../charts/AbstractWeekChartFragment.java | 4 +++ .../charts/AngledLabelsChartRenderer.java | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java index 0af2e32f9..744fe5134 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java @@ -88,6 +88,10 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { setupLegend(mWeekChart); mTodayPieChart.setCenterText(mcd.getDayData().centerText); mTodayPieChart.setData(mcd.getDayData().data); + //set custom renderer for 30days bar charts + if (GBApplication.getPrefs().getBoolean("charts_range", true)) { + mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), mWeekChart.getViewPortHandler())); + } mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 mWeekChart.setData(mcd.getWeekBeforeData().getData()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java new file mode 100644 index 000000000..d95708b6f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java @@ -0,0 +1,31 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.graphics.Canvas; + +import com.github.mikephil.charting.animation.ChartAnimator; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; +import com.github.mikephil.charting.renderer.BarChartRenderer; +import com.github.mikephil.charting.utils.ViewPortHandler; + +public class AngledLabelsChartRenderer extends BarChartRenderer { + public AngledLabelsChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + super(chart, animator, viewPortHandler); + } + + @Override + public void drawValue(Canvas canvas, IValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) { + + mValuePaint.setColor(color); + + //move position to the center of bar + x=x+8; + y=y-25; + + canvas.save(); + canvas.rotate(-90, x, y); + + canvas.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint); + canvas.restore(); + }} From a69a139602fd6d2cb364f08620064450c2dbea58 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 24 Aug 2019 12:41:26 +0200 Subject: [PATCH 008/154] Migrate to upstream MPAndroidChart v3.1.0 --- app/build.gradle | 2 +- .../activities/charts/AbstractChartFragment.java | 13 +++++++------ .../charts/AbstractWeekChartFragment.java | 9 ++++----- .../charts/ActivitySleepChartFragment.java | 2 +- .../charts/AngledLabelsChartRenderer.java | 9 ++++----- .../activities/charts/SleepChartFragment.java | 5 +++-- .../activities/charts/TimestampValueFormatter.java | 3 ++- .../activities/charts/WeekSleepChartFragment.java | 14 +++++++------- .../activities/charts/WeekStepsChartFragment.java | 9 ++++----- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7121ef0c6..64749243b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,7 +79,7 @@ dependencies { exclude group: "com.google.android", module: "android" } implementation "org.slf4j:slf4j-api:1.7.12" - implementation "com.github.Freeyourgadget:MPAndroidChart:5e5bd6c1d3e95c515d4853647ae554e48ee1d593" + implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.pfichtner:durationformatter:0.1.1" implementation "de.cketti.library.changelog:ckchangelog:1.2.2" implementation "net.e175.klaus:solarpositioning:0.0.9" diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index b0722bd7d..6bf3f738d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -36,6 +36,7 @@ import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import org.slf4j.Logger; @@ -572,7 +573,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { lineData = new LineData(); } - IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation); + ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation); return new DefaultChartsData(lineData, xValueFormatter); } @@ -753,14 +754,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { public static class DefaultChartsData> extends ChartsData { private final T data; - private IAxisValueFormatter xValueFormatter; + private ValueFormatter xValueFormatter; - public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) { + public DefaultChartsData(T data, ValueFormatter xValueFormatter) { this.xValueFormatter = xValueFormatter; this.data = data; } - public IAxisValueFormatter getXValueFormatter() { + public ValueFormatter getXValueFormatter() { return xValueFormatter; } @@ -769,7 +770,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } } - protected static class SampleXLabelFormatter implements IAxisValueFormatter { + protected static class SampleXLabelFormatter extends ValueFormatter { private final TimestampTranslation tsTranslation; SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm"); // SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); @@ -791,7 +792,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } } - protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter { + protected static class PreformattedXIndexLabelFormatter extends ValueFormatter { private ArrayList xLabels; public PreformattedXIndexLabelFormatter(ArrayList xLabels) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java index 744fe5134..8dcbaed2e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java @@ -37,8 +37,7 @@ import com.github.mikephil.charting.data.ChartData; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.data.PieEntry; -import com.github.mikephil.charting.formatter.IAxisValueFormatter; -import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -372,11 +371,11 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { abstract String[] getPieLabels(); - abstract IValueFormatter getPieValueFormatter(); + abstract ValueFormatter getPieValueFormatter(); - abstract IValueFormatter getBarValueFormatter(); + abstract ValueFormatter getBarValueFormatter(); - abstract IAxisValueFormatter getYAxisFormatter(); + abstract ValueFormatter getYAxisFormatter(); abstract int[] getColors(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java index 4c4e1000f..5bdd11ae8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java @@ -143,7 +143,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { @Override protected void renderCharts() { - mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart); + mChart.animateX(ANIM_TIME, Easing.EaseInOutQuart); // mChart.invalidate(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java index d95708b6f..286d484c0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AngledLabelsChartRenderer.java @@ -3,19 +3,17 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.graphics.Canvas; import com.github.mikephil.charting.animation.ChartAnimator; -import com.github.mikephil.charting.data.Entry; -import com.github.mikephil.charting.formatter.IValueFormatter; import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; import com.github.mikephil.charting.renderer.BarChartRenderer; import com.github.mikephil.charting.utils.ViewPortHandler; public class AngledLabelsChartRenderer extends BarChartRenderer { - public AngledLabelsChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { + AngledLabelsChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) { super(chart, animator, viewPortHandler); } @Override - public void drawValue(Canvas canvas, IValueFormatter formatter, float value, Entry entry, int dataSetIndex, float x, float y, int color) { + public void drawValue(Canvas canvas, String valueText, float x, float y, int color) { mValuePaint.setColor(color); @@ -26,6 +24,7 @@ public class AngledLabelsChartRenderer extends BarChartRenderer { canvas.save(); canvas.rotate(-90, x, y); - canvas.drawText(formatter.getFormattedValue(value, entry, dataSetIndex, mViewPortHandler), x, y, mValuePaint); + canvas.drawText(valueText, x, y, mValuePaint); + canvas.restore(); }} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java index 05b090368..1d64cae3d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java @@ -38,6 +38,7 @@ import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.data.PieEntry; import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.utils.ViewPortHandler; import org.slf4j.Logger; @@ -118,7 +119,7 @@ public class SleepChartFragment extends AbstractChartFragment { } String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS); PieDataSet set = new PieDataSet(entries, ""); - set.setValueFormatter(new IValueFormatter() { + set.setValueFormatter(new ValueFormatter() { @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS); @@ -269,7 +270,7 @@ public class SleepChartFragment extends AbstractChartFragment { @Override protected void renderCharts() { - mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart); + mActivityChart.animateX(ANIM_TIME, Easing.EaseInOutQuart); mSleepAmountChart.invalidate(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java index e6bfdd55c..cbb62eb11 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import com.github.mikephil.charting.components.AxisBase; import com.github.mikephil.charting.formatter.IAxisValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -25,7 +26,7 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -public class TimestampValueFormatter implements IAxisValueFormatter { +public class TimestampValueFormatter extends ValueFormatter { private final Calendar cal; // private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); private DateFormat dateFormat; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java index 04ed240a6..155f31c3b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java @@ -22,7 +22,7 @@ import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.LegendEntry; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.formatter.IAxisValueFormatter; -import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.utils.ViewPortHandler; import java.util.ArrayList; @@ -115,8 +115,8 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { } @Override - IValueFormatter getPieValueFormatter() { - return new IValueFormatter() { + ValueFormatter getPieValueFormatter() { + return new ValueFormatter() { @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { return formatPieValue((long) value); @@ -125,8 +125,8 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { } @Override - IValueFormatter getBarValueFormatter() { - return new IValueFormatter() { + ValueFormatter getBarValueFormatter() { + return new ValueFormatter() { @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { return DateTimeUtils.minutesToHHMM((int) value); @@ -135,8 +135,8 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { } @Override - IAxisValueFormatter getYAxisFormatter() { - return new IAxisValueFormatter() { + ValueFormatter getYAxisFormatter() { + return new ValueFormatter() { @Override public String getFormattedValue(float value, AxisBase axis) { return DateTimeUtils.minutesToHHMM((int) value); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java index 24f02940d..ec720a6da 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekStepsChartFragment.java @@ -18,8 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import com.github.mikephil.charting.charts.Chart; -import com.github.mikephil.charting.formatter.IAxisValueFormatter; -import com.github.mikephil.charting.formatter.IValueFormatter; +import com.github.mikephil.charting.formatter.ValueFormatter; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -82,17 +81,17 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment { } @Override - IValueFormatter getPieValueFormatter() { + ValueFormatter getPieValueFormatter() { return null; } @Override - IValueFormatter getBarValueFormatter() { + ValueFormatter getBarValueFormatter() { return null; } @Override - IAxisValueFormatter getYAxisFormatter() { + ValueFormatter getYAxisFormatter() { return null; } From 984db60c5f4408eb71e258977a27fbd29d07984b Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 24 Aug 2019 12:55:33 +0200 Subject: [PATCH 009/154] Fix formatted values for charts --- .../activities/charts/AbstractChartFragment.java | 15 +++++++-------- .../activities/charts/SleepChartFragment.java | 8 +++----- .../charts/TimestampValueFormatter.java | 4 +--- .../activities/charts/WeekSleepChartFragment.java | 10 +++------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index 6bf3f738d..3c49dbfa2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -26,16 +26,19 @@ import android.os.Bundle; import android.util.TypedValue; import android.view.View; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; -import com.github.mikephil.charting.components.AxisBase; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.ChartData; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; -import com.github.mikephil.charting.formatter.IAxisValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; @@ -52,10 +55,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; @@ -782,7 +781,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } // TODO: this does not work. Cannot use precomputed labels @Override - public String getFormattedValue(float value, AxisBase axis) { + public String getFormattedValue(float value) { cal.clear(); int ts = (int) value; cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L); @@ -800,7 +799,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } @Override - public String getFormattedValue(float value, AxisBase axis) { + public String getFormattedValue(float value) { int index = (int) value; if (xLabels == null || index >= xLabels.size()) { return String.valueOf(value); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java index 1d64cae3d..e28011701 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java @@ -25,6 +25,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; @@ -32,14 +34,11 @@ import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.components.LegendEntry; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; -import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.PieData; import com.github.mikephil.charting.data.PieDataSet; import com.github.mikephil.charting.data.PieEntry; -import com.github.mikephil.charting.formatter.IValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter; -import com.github.mikephil.charting.utils.ViewPortHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +48,6 @@ import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; -import androidx.annotation.Nullable; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; @@ -121,7 +119,7 @@ public class SleepChartFragment extends AbstractChartFragment { PieDataSet set = new PieDataSet(entries, ""); set.setValueFormatter(new ValueFormatter() { @Override - public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + public String getFormattedValue(float value) { return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS); } }); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java index cbb62eb11..a79af0781 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampValueFormatter.java @@ -16,8 +16,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; -import com.github.mikephil.charting.components.AxisBase; -import com.github.mikephil.charting.formatter.IAxisValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter; import java.text.DateFormat; @@ -43,7 +41,7 @@ public class TimestampValueFormatter extends ValueFormatter { } @Override - public String getFormattedValue(float value, AxisBase axis) { + public String getFormattedValue(float value) { cal.setTimeInMillis((int) value * 1000L); Date date = cal.getTime(); String dateString = dateFormat.format(date); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java index 155f31c3b..62018cd18 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java @@ -17,13 +17,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import com.github.mikephil.charting.charts.Chart; -import com.github.mikephil.charting.components.AxisBase; import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.LegendEntry; -import com.github.mikephil.charting.data.Entry; -import com.github.mikephil.charting.formatter.IAxisValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter; -import com.github.mikephil.charting.utils.ViewPortHandler; import java.util.ArrayList; import java.util.List; @@ -118,7 +114,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { ValueFormatter getPieValueFormatter() { return new ValueFormatter() { @Override - public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + public String getFormattedValue(float value) { return formatPieValue((long) value); } }; @@ -128,7 +124,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { ValueFormatter getBarValueFormatter() { return new ValueFormatter() { @Override - public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { + public String getFormattedValue(float value) { return DateTimeUtils.minutesToHHMM((int) value); } }; @@ -138,7 +134,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { ValueFormatter getYAxisFormatter() { return new ValueFormatter() { @Override - public String getFormattedValue(float value, AxisBase axis) { + public String getFormattedValue(float value) { return DateTimeUtils.minutesToHHMM((int) value); } }; From d07ca6faa6b3debfe5d465d2db2e80c0e6fecbf7 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 25 Aug 2019 09:55:23 +0200 Subject: [PATCH 010/154] Mi Band 4: Fix location not being updated on the Band Also move generic code from AmazfitBipSupport to HuamiSupport where is belongs Fixes #1609 --- .../service/devices/huami/HuamiSupport.java | 190 ++++++++++++++++- .../huami/amazfitbip/AmazfitBipSupport.java | 195 +----------------- 2 files changed, 190 insertions(+), 195 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 9d0c5b14d..b8f1f7624 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -42,11 +42,13 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.SimpleTimeZone; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import java.util.concurrent.TimeUnit; +import cyanogenmod.weather.util.WeatherUtils; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.R; @@ -66,6 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2FWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; @@ -95,6 +98,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; @@ -762,9 +766,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } + private void sendMusicStateToDevice() { - - if (characteristicChunked == null) { return; } @@ -1584,7 +1587,190 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { @Override public void onSendWeather(WeatherSpec weatherSpec) { + // FIXME: currently HuamiSupport *is* MiBand2 support, so return if we are using Mi Band 2 + if (gbDevice.getType() == DeviceType.MIBAND2) { + return; + } + if (gbDevice.getFirmwareVersion() == null) { + LOG.warn("Device not initialized yet, so not sending weather info"); + return; + } + boolean supportsConditionString = false; + + Version version = new Version(gbDevice.getFirmwareVersion()); + if (version.compareTo(new Version("0.0.8.74")) >= 0) { + supportsConditionString = true; + } + + MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit(); + int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60); + try { + TransactionBuilder builder; + builder = performInitialized("Sending current temp"); + + byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode); + + int length = 8; + if (supportsConditionString) { + length += weatherSpec.currentCondition.getBytes().length + 1; + } + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.put((byte) 2); + buf.putInt(weatherSpec.timestamp); + buf.put((byte) (tz_offset_hours * 4)); + buf.put(condition); + + int currentTemp = weatherSpec.currentTemp - 273; + if (unit == MiBandConst.DistanceUnit.IMPERIAL) { + currentTemp = (int) WeatherUtils.celsiusToFahrenheit(currentTemp); + } + buf.put((byte) currentTemp); + + if (supportsConditionString) { + buf.put(weatherSpec.currentCondition.getBytes()); + buf.put((byte) 0); + } + + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + + builder.queue(getQueue()); + } catch (Exception ex) { + LOG.error("Error sending current weather", ex); + } + + try { + TransactionBuilder builder; + builder = performInitialized("Sending air quality index"); + int length = 8; + String aqiString = "(n/a)"; + if (supportsConditionString) { + length += aqiString.getBytes().length + 1; + } + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 4); + buf.putInt(weatherSpec.timestamp); + buf.put((byte) (tz_offset_hours * 4)); + buf.putShort((short) 0); + if (supportsConditionString) { + buf.put(aqiString.getBytes()); + buf.put((byte) 0); + } + + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Error sending air quality"); + } + + try { + TransactionBuilder builder = performInitialized("Sending weather forecast"); + + final byte NR_DAYS = (byte) (1 + weatherSpec.forecasts.size()); + int bytesPerDay = 4; + + int conditionsLength = 0; + if (supportsConditionString) { + bytesPerDay = 5; + conditionsLength = weatherSpec.currentCondition.getBytes().length; + for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) { + conditionsLength += Weather.getConditionString(forecast.conditionCode).getBytes().length; + } + } + + int length = 7 + bytesPerDay * NR_DAYS + conditionsLength; + ByteBuffer buf = ByteBuffer.allocate(length); + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 1); + buf.putInt(weatherSpec.timestamp); + buf.put((byte) (tz_offset_hours * 4)); + + buf.put(NR_DAYS); + + byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode); + buf.put(condition); + buf.put(condition); + + int todayMaxTemp = weatherSpec.todayMaxTemp - 273; + int todayMinTemp = weatherSpec.todayMinTemp - 273; + if (unit == MiBandConst.DistanceUnit.IMPERIAL) { + todayMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMaxTemp); + todayMinTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMinTemp); + } + buf.put((byte) todayMaxTemp); + buf.put((byte) todayMinTemp); + + if (supportsConditionString) { + buf.put(weatherSpec.currentCondition.getBytes()); + buf.put((byte) 0); + } + + for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) { + condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode); + buf.put(condition); + buf.put(condition); + + int forecastMaxTemp = forecast.maxTemp - 273; + int forecastMinTemp = forecast.minTemp - 273; + if (unit == MiBandConst.DistanceUnit.IMPERIAL) { + forecastMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMaxTemp); + forecastMinTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMinTemp); + } + buf.put((byte) forecastMaxTemp); + buf.put((byte) forecastMinTemp); + + if (supportsConditionString) { + buf.put(Weather.getConditionString(forecast.conditionCode).getBytes()); + buf.put((byte) 0); + } + } + + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + + builder.queue(getQueue()); + } catch (Exception ex) { + LOG.error("Error sending weather forecast", ex); + } + + try { + TransactionBuilder builder; + builder = performInitialized("Sending forecast location"); + + int length = 2 + weatherSpec.location.getBytes().length; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 8); + buf.put(weatherSpec.location.getBytes()); + buf.put((byte) 0); + + + if (characteristicChunked != null) { + writeToChunked(builder, 4, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + + builder.queue(getQueue()); + } catch (Exception ex) { + LOG.error("Error sending current forecast location", ex); + } } private HuamiSupport setDateDisplay(TransactionBuilder builder) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java index e67c6532e..469e360c7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java @@ -28,39 +28,30 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.Set; -import java.util.SimpleTimeZone; import java.util.UUID; -import cyanogenmod.weather.util.WeatherUtils; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; -import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; -import nodomain.freeyourgadget.gadgetbridge.model.Weather; -import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; -import nodomain.freeyourgadget.gadgetbridge.util.Version; public class AmazfitBipSupport extends HuamiSupport { @@ -248,187 +239,6 @@ public class AmazfitBipSupport extends HuamiSupport { builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command); } - @Override - public void onSendWeather(WeatherSpec weatherSpec) { - if (gbDevice.getFirmwareVersion() == null) { - LOG.warn("Device not initialized yet, so not sending weather info"); - return; - } - boolean supportsConditionString = false; - - Version version = new Version(gbDevice.getFirmwareVersion()); - if (version.compareTo(new Version("0.0.8.74")) >= 0) { - supportsConditionString = true; - } - - MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit(); - int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60); - try { - TransactionBuilder builder; - builder = performInitialized("Sending current temp"); - - byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode); - - int length = 8; - if (supportsConditionString) { - length += weatherSpec.currentCondition.getBytes().length + 1; - } - ByteBuffer buf = ByteBuffer.allocate(length); - buf.order(ByteOrder.LITTLE_ENDIAN); - - buf.put((byte) 2); - buf.putInt(weatherSpec.timestamp); - buf.put((byte) (tz_offset_hours * 4)); - buf.put(condition); - - int currentTemp = weatherSpec.currentTemp - 273; - if (unit == MiBandConst.DistanceUnit.IMPERIAL) { - currentTemp = (int) WeatherUtils.celsiusToFahrenheit(currentTemp); - } - buf.put((byte) currentTemp); - - if (supportsConditionString) { - buf.put(weatherSpec.currentCondition.getBytes()); - buf.put((byte) 0); - } - - if (characteristicChunked != null) { - writeToChunked(builder, 1, buf.array()); - } else { - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); - } - - builder.queue(getQueue()); - } catch (Exception ex) { - LOG.error("Error sending current weather", ex); - } - - if (gbDevice.getType() != DeviceType.AMAZFITCOR) { - try { - TransactionBuilder builder; - builder = performInitialized("Sending air quality index"); - int length = 8; - String aqiString = "(n/a)"; - if (supportsConditionString) { - length += aqiString.getBytes().length + 1; - } - ByteBuffer buf = ByteBuffer.allocate(length); - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put((byte) 4); - buf.putInt(weatherSpec.timestamp); - buf.put((byte) (tz_offset_hours * 4)); - buf.putShort((short) 0); - if (supportsConditionString) { - buf.put(aqiString.getBytes()); - buf.put((byte) 0); - } - - if (characteristicChunked != null) { - writeToChunked(builder, 1, buf.array()); - } else { - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); - } - - builder.queue(getQueue()); - } catch (IOException ex) { - LOG.error("Error sending air quality"); - } - } - - try { - TransactionBuilder builder = performInitialized("Sending weather forecast"); - - final byte NR_DAYS = (byte) (1 + weatherSpec.forecasts.size()); - int bytesPerDay = 4; - - int conditionsLength = 0; - if (supportsConditionString) { - bytesPerDay = 5; - conditionsLength = weatherSpec.currentCondition.getBytes().length; - for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) { - conditionsLength += Weather.getConditionString(forecast.conditionCode).getBytes().length; - } - } - - int length = 7 + bytesPerDay * NR_DAYS + conditionsLength; - ByteBuffer buf = ByteBuffer.allocate(length); - - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put((byte) 1); - buf.putInt(weatherSpec.timestamp); - buf.put((byte) (tz_offset_hours * 4)); - - buf.put(NR_DAYS); - - byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode); - buf.put(condition); - buf.put(condition); - - int todayMaxTemp = weatherSpec.todayMaxTemp - 273; - int todayMinTemp = weatherSpec.todayMinTemp - 273; - if (unit == MiBandConst.DistanceUnit.IMPERIAL) { - todayMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMaxTemp); - todayMinTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMinTemp); - } - buf.put((byte) todayMaxTemp); - buf.put((byte) todayMinTemp); - - if (supportsConditionString) { - buf.put(weatherSpec.currentCondition.getBytes()); - buf.put((byte) 0); - } - - for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) { - condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode); - buf.put(condition); - buf.put(condition); - - int forecastMaxTemp = forecast.maxTemp - 273; - int forecastMinTemp = forecast.minTemp - 273; - if (unit == MiBandConst.DistanceUnit.IMPERIAL) { - forecastMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMaxTemp); - forecastMinTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMinTemp); - } - buf.put((byte) forecastMaxTemp); - buf.put((byte) forecastMinTemp); - - if (supportsConditionString) { - buf.put(Weather.getConditionString(forecast.conditionCode).getBytes()); - buf.put((byte) 0); - } - } - - if (characteristicChunked != null) { - writeToChunked(builder, 1, buf.array()); - } else { - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); - } - - builder.queue(getQueue()); - } catch (Exception ex) { - LOG.error("Error sending weather forecast", ex); - } - - if (gbDevice.getType() == DeviceType.AMAZFITCOR) { - try { - TransactionBuilder builder; - builder = performInitialized("Sending forecast location"); - - int length = 2 + weatherSpec.location.getBytes().length; - ByteBuffer buf = ByteBuffer.allocate(length); - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put((byte) 8); - buf.put(weatherSpec.location.getBytes()); - buf.put((byte) 0); - - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); - builder.queue(getQueue()); - } catch (Exception ex) { - LOG.error("Error sending current forecast location", ex); - } - } - } - @Override public void onFetchRecordedData(int dataTypes) { try { @@ -439,8 +249,7 @@ public class AmazfitBipSupport extends HuamiSupport { new FetchSportsSummaryOperation(this).perform(); } else if (dataTypes == RecordedDataTypes.TYPE_DEBUGLOGS) { new HuamiFetchDebugLogsOperation(this).perform(); - } - else { + } else { LOG.warn("fetching multiple data types at once is not supported yet"); } } catch (IOException ex) { From 4663dbf0b9664dd5152efa06809cbd4c84308c2e Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 25 Aug 2019 09:58:18 +0200 Subject: [PATCH 011/154] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4406b2393..d6f7ccdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect) * Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise" * Mi Band 4: Support flashing the V2 font +* Mi Band 4: Fix weather location not being updated on the Band * Mi Band 4: remove unsupported DND setting from settings menu * Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities * Amazfit Bip: Fix sharing GPX files for some Apps From da2e073dc29d7efd8801fad9a038dae6e2427b02 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 25 Aug 2019 20:23:53 +0200 Subject: [PATCH 012/154] Mi Band 4: Whitelist latest stable and beta firmware --- .../service/devices/huami/miband4/MiBand4FirmwareInfo.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java index 38987a3b2..15d5a4790 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband4/MiBand4FirmwareInfo.java @@ -38,9 +38,13 @@ public class MiBand4FirmwareInfo extends HuamiFirmwareInfo { static { // firmware crcToVersion.put(8969, "1.0.5.22"); + crcToVersion.put(43437, "1.0.5.66"); + crcToVersion.put(31632, "1.0.6.00"); // resources crcToVersion.put(27412, "1.0.5.22"); + crcToVersion.put(5466, "1.0.5.66"); + crcToVersion.put(20047, "1.0.6.00"); // font crcToVersion.put(31978, "1"); From a8b53bdecab96c6df25e8d72c7f058ffbf4bbdfd Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 25 Aug 2019 20:43:49 +0200 Subject: [PATCH 013/154] bump version add changelogs --- CHANGELOG.md | 3 ++- app/build.gradle | 4 ++-- app/src/main/res/xml/changelog_master.xml | 15 +++++++++++++++ .../metadata/android/en-US/changelogs/155.txt | 13 +++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/155.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f7ccdf1..2eabecee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### Changelog -#### NEXT +#### Version 0.36.0 * Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time * Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now) * Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect) @@ -11,6 +11,7 @@ * Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities * Amazfit Bip: Fix sharing GPX files for some Apps * Pebble: Use Rebble Store URI +* Support LineageOS 16.0 weather provider * Add Averages to Charts * Allow togging between weekly and monthly charts diff --git a/app/build.gradle b/app/build.gradle index 64749243b..60cfbf04e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,8 +25,8 @@ android { targetSdkVersion 27 // Note: always bump BOTH versionCode and versionName! - versionName "0.35.2" - versionCode 154 + versionName "0.36.0" + versionCode 155 vectorDrawables.useSupportLibrary = true } buildTypes { diff --git a/app/src/main/res/xml/changelog_master.xml b/app/src/main/res/xml/changelog_master.xml index bb136ae81..8de96c4ff 100644 --- a/app/src/main/res/xml/changelog_master.xml +++ b/app/src/main/res/xml/changelog_master.xml @@ -1,5 +1,20 @@ + + Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time + Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now) + Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect) + Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise" + Mi Band 4: Support flashing the V2 font + Mi Band 4: Fix weather location not being updated on the Band + Mi Band 4: remove unsupported DND setting from settings menu + Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities + Amazfit Bip: Fix sharing GPX files for some Apps + Pebble: Use Rebble Store URI + Support LineageOS 16.0 weather provider + Add Averages to Charts + Allow togging between weekly and monthly charts + Mi Band 1/2: Crash when updating firmware while phone is set to Spanish Mi Band 4: Enable music info support (displays now on the band) diff --git a/fastlane/metadata/android/en-US/changelogs/155.txt b/fastlane/metadata/android/en-US/changelogs/155.txt new file mode 100644 index 000000000..e90ce4900 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/155.txt @@ -0,0 +1,13 @@ +* Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time +* Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now) +* Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect) +* Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise" +* Mi Band 4: Support flashing the V2 font +* Mi Band 4: Fix weather location not being updated on the Band +* Mi Band 4: remove unsupported DND setting from settings menu +* Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities +* Amazfit Bip: Fix sharing GPX files for some Apps +* Pebble: Use Rebble Store URI +* Support LineageOS 16.0 weather provider +* Add Averages to Charts +* Allow togging between weekly and monthly charts From 759a04b5377394bad19fed3cbc91b2ea057bae05 Mon Sep 17 00:00:00 2001 From: Rafael Fontenelle Date: Thu, 22 Aug 2019 11:45:36 +0000 Subject: [PATCH 014/154] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (681 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 719b006fa..ad8214d42 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -728,4 +728,14 @@ Alarme de frequência cardíaca durante atividades esportivas Limite baixo Limite alto + Média: %1$s + Configurações dos gráficos + Mostrar médias nos gráficos + Intervalo de gráficos + O intervalo de gráficos está definido para um Mês + O intervalo de gráficos está definido para uma Semana + Passos por mês + Sono por mês + Relógio inteligente Mijia + NFC \ No newline at end of file From 94832e713514215fd9a0dcce8d23580eb34fe108 Mon Sep 17 00:00:00 2001 From: Full Name Date: Thu, 22 Aug 2019 19:43:47 +0000 Subject: [PATCH 015/154] Translated using Weblate (Czech) Currently translated at 100.0% (681 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/cs/ --- app/src/main/res/values-cs/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 26cbd5288..3fe29dd98 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -721,4 +721,14 @@ Alarm měření tepu při sportovní aktivitě Spodní hodnota Horní limit + Průměr: %1$s + Grafy + Zobrazovat v grafech průměry + Časové období grafů + Rozsah nastaven na měsíc + Rozsah nastaven na týden + Kroky za měsíc + Spánek za měsíc + Mijia Smart Clock + NFC \ No newline at end of file From e25f1bf0010ca35a8cdc824c4262e6ecb5f865a3 Mon Sep 17 00:00:00 2001 From: Yaron Shahrabani Date: Thu, 22 Aug 2019 20:12:37 +0000 Subject: [PATCH 016/154] Translated using Weblate (Hebrew) Currently translated at 100.0% (681 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/he/ --- app/src/main/res/values-he/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 2bf609b74..0867bcf5e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -717,4 +717,14 @@ התראת דופק במהלך פעילות ספורטיבית גבול תחתון גבול עליון + ממוצע: %1$s + הגדרות תרשימים + הצגת ממוצעים בתרשימים + טווח התרשימים + טווח התרשימים מוגדר לחודש + טווח התרשימים מוגדר לשבוע + צעדים בחודש + שינה בחודש + שעון קיר חכם מבית Mijia + תקשורת קרבה \ No newline at end of file From ade5eb927f1438ccf20a69922e49ca16eede1895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=B0=91=E4=B8=BE?= Date: Thu, 22 Aug 2019 12:21:47 +0000 Subject: [PATCH 017/154] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (681 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 123d85ccb..46169ef76 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -722,4 +722,14 @@ 运动期间的心率警报 下限 上限 + 平均:%1$s + 图表设置 + 在图表中显示平均值 + 图表范围 + 图表范围已设置为按月 + 图表范围已设置为按周 + 每月步数 + 每月睡眠 + 米家智能闹钟 + NFC \ No newline at end of file From 5adbd68afa2d6d57c8f21e8a61d1e1a406668b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Thu, 22 Aug 2019 14:34:20 +0000 Subject: [PATCH 018/154] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.2% (669 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/nb_NO/ --- app/src/main/res/values-nb-rNO/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 04b687a26..e0e5989d3 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -722,4 +722,14 @@ Pulsalarm under sportsaktiviteter Nedre grense Øvre grense + Gjennomsnitt: %1$s + Poengtavleinnstillinger + Vis gjennomsnitt i poengtavlene + Poengtavleområde + Poengtavleområde satt til én måned + Poengtavleområde satt til ei uke + Steg per måned + Søvn per måned + Mijia-smartklokke + NFC \ No newline at end of file From 4e133176096b8a84425a3adee997353d284c7ad4 Mon Sep 17 00:00:00 2001 From: Swann Martinet Date: Sat, 24 Aug 2019 00:08:42 +0000 Subject: [PATCH 019/154] Translated using Weblate (French) Currently translated at 99.4% (677 of 681 strings) Translation: Freeyourgadget/Gadgetbridge Translate-URL: https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/fr/ --- app/src/main/res/values-fr/strings.xml | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index da61eb43b..fbad9976c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -333,7 +333,7 @@ Poids en kg Authentification Authentification requise - ZzZz + Zzz Ajouter un widget Temps de sommeil préféré en heures @@ -518,11 +518,17 @@ Temps de sommeil préféré en heures Mettre toutes les notifications en liste noire Mettre toutes les notifications en liste blanche "Vous allez installer le micrologiciel %s dans votre Mi Band 3. +\n \n +\n \nAssurez-vous d\'installer d\'abord le fichier .fw, et ensuite le fichiers .res. Votre bracelet redémarrera après l\'installation du fichiers .fw. +\n \n -\nRemarque: Vous n\'avez pas besoin d\'installer le fichier .res si c\'est exactement le même que celui déjà installé. +\n +\nRemarque : Vous n\'avez pas besoin d\'installer le fichier .res si c\'est exactement le même que celui déjà installé. +\n \n +\n \nÀ VOS RISQUES ET PÉRILS !" client GATT uniquement C\'est uniquement pour les Pebble 2 et expérimental, à essayer si vous avez des problèmes de connectivité @@ -707,4 +713,22 @@ Temps de sommeil préféré en heures Portugais Amazfit Cor 2 Allonge ou raccourcis les lignes dans les textes écrits de droite à gauche + Mouvement de la main + Mi Band 4 + Vous êtes sur le point d\'installer le firmware %s sur votre Mi Band 4. +\n +\nVeuillez vous assurez d\'installer le fichier .fw, et après que le fichier .res. Votre bracelet redémarrera après l\'installation du fichier .fw. +\n +\nRemarque : Vous n\'avez pas à installer .res s\'il est exactement le même que celui précédemment installé. +\n +\nÀ VOS RISQUES ET PÉRILS ! + Alarme de la fréquence cardiaque durant une activité sportive + Limite basse + Limite haute + Moyenne : %1$s + Paramètres graphiques + Afficher les moyennes dans les graphiques + Gamme des graphiques + Pas par mois + Sommeil par mois \ No newline at end of file From 243eec042d497ff47b7a8a7e68a42eb6d6e38e80 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Tue, 27 Aug 2019 11:13:45 +0200 Subject: [PATCH 020/154] Mi Band 3/4, Amazfit Bip/Cor: Add setting to expose the HR sensor to 3rd party apps Closes #1606 --- .../DeviceSpecificSettingsFragment.java | 2 ++ .../gadgetbridge/devices/huami/HuamiConst.java | 2 +- .../devices/huami/HuamiCoordinator.java | 5 +++++ .../devices/huami/HuamiService.java | 2 ++ .../amazfitbip/AmazfitBipCoordinator.java | 1 + .../amazfitcor/AmazfitCorCoordinator.java | 4 +++- .../huami/miband3/MiBand3Coordinator.java | 1 + .../huami/miband4/MiBand4Coordinator.java | 1 + .../service/devices/huami/HuamiSupport.java | 18 ++++++++++++++++++ app/src/main/res/values/strings.xml | 3 +++ .../devicesettings_expose_hr_thirdparty.xml | 9 +++++++++ 11 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/xml/devicesettings_expose_hr_thirdparty.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 83aa6ab2f..d18dcb9e8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -29,6 +29,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_END; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_START; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_LANGUAGE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_END; @@ -287,6 +288,7 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { addPreferenceHandlerFor(PREF_DATEFORMAT); addPreferenceHandlerFor(PREF_DISPLAY_ITEMS); addPreferenceHandlerFor(PREF_LANGUAGE); + addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY); String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF); boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java index d595f37f0..fe47cb21f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java @@ -61,7 +61,7 @@ public class HuamiConst { public static final String PREF_DISPLAY_ITEMS = "display_items"; public static final String PREF_LANGUAGE = "language"; public static final String PREF_DATEFORMAT = "dateformat"; - + public static final String PREF_EXPOSE_HR_THIRDPARTY = "expose_hr_thirdparty"; public static int toActivityKind(int rawType) { switch (rawType) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index 0c1951c58..d7bf1d353 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -251,6 +251,11 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator { return prefs.getBoolean(MiBandConst.PREF_SWIPE_UNLOCK, false); } + public static boolean getExposeHRThirdParty(String deviceAddress) { + Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress)); + return prefs.getBoolean(HuamiConst.PREF_EXPOSE_HR_THIRDPARTY, false); + } + protected static Date getTimePreference(String key, String defaultValue, String deviceAddress) { Prefs prefs; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java index 86fd89ff2..b18466afc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java @@ -139,6 +139,8 @@ public class HuamiService { public static final byte[] DATEFORMAT_TIME_12_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x0 }; public static final byte[] DATEFORMAT_TIME_24_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x1 }; public static final byte[] DATEFORMAT_DATE_MM_DD_YYYY = new byte[]{ENDPOINT_DISPLAY, 30, 0x00, 'M', 'M', '/', 'd', 'd', '/', 'y', 'y', 'y', 'y'}; + public static final byte[] COMMAND_ENBALE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x01}; + public static final byte[] COMMAND_DISABLE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x00}; public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01}; public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x00}; public static final byte[] COMMAND_SCHEDULE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java index 547a9be75..1971dd48c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java @@ -83,6 +83,7 @@ public class AmazfitBipCoordinator extends HuamiCoordinator { R.xml.devicesettings_amazfitbip, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_disconnectnotification, + R.xml.devicesettings_expose_hr_thirdparty, R.xml.devicesettings_pairingkey }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java index 0fa0fdbda..96ac2e9fc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java @@ -86,6 +86,8 @@ public class AmazfitCorCoordinator extends HuamiCoordinator { R.xml.devicesettings_amazfitcor, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_disconnectnotification, - R.xml.devicesettings_pairingkey}; + R.xml.devicesettings_expose_hr_thirdparty, + R.xml.devicesettings_pairingkey + }; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java index 4c2404159..ad757c876 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java @@ -108,6 +108,7 @@ public class MiBand3Coordinator extends HuamiCoordinator { R.xml.devicesettings_donotdisturb_withauto, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_swipeunlock, + R.xml.devicesettings_expose_hr_thirdparty, R.xml.devicesettings_pairingkey }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java index 31c0796d6..fba3a5856 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java @@ -92,6 +92,7 @@ public class MiBand4Coordinator extends HuamiCoordinator { R.xml.devicesettings_nightmode, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_swipeunlock, + R.xml.devicesettings_expose_hr_thirdparty, R.xml.devicesettings_pairingkey }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index b8f1f7624..8accd9e62 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -1568,6 +1568,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { case HuamiConst.PREF_LANGUAGE: setLanguage(builder); break; + case HuamiConst.PREF_EXPOSE_HR_THIRDPARTY: + setExposeHRThridParty(builder); + break; } builder.queue(getQueue()); } catch (IOException e) { @@ -2095,6 +2098,20 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { return this; } + + private HuamiSupport setExposeHRThridParty(TransactionBuilder builder) { + boolean enable = HuamiCoordinator.getExposeHRThirdParty(gbDevice.getAddress()); + LOG.info("Setting exposure of HR to third party apps to: " + enable); + + if (enable) { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENBALE_HR_CONNECTION); + } else { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_HR_CONNECTION); + } + + return this; + } + protected void writeToChunked(TransactionBuilder builder, int type, byte[] data) { final int MAX_CHUNKLENGTH = 17; int remaining = data.length; @@ -2145,6 +2162,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { setInactivityWarnings(builder); setHeartrateSleepSupport(builder); setDisconnectNotification(builder); + setExposeHRThridParty(builder); setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval()); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7eb31a4c..16289fdc8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -165,6 +165,9 @@ Enable background JS When enabled, allows watchfaces to show weather, battery info etc. Reconnection attempts + Allows other apps to access HR data in realtime while Gadgetbridge is connected + 3rd party realtime HR access + Units Time format diff --git a/app/src/main/res/xml/devicesettings_expose_hr_thirdparty.xml b/app/src/main/res/xml/devicesettings_expose_hr_thirdparty.xml new file mode 100644 index 000000000..4a5ab035c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_expose_hr_thirdparty.xml @@ -0,0 +1,9 @@ + + + + From b38b83377cf1ffac6a74490488e8236d426f8281 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Wed, 28 Aug 2019 10:11:15 +0200 Subject: [PATCH 021/154] Mi Band 2: enable third party hr sensor access setting This works but since the Mi Band 2 has no "status" menu or activities, live activity has to be started (charts swipe to right), but that spams the database... --- .../gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java index add706c68..a8bd207a6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java @@ -87,6 +87,7 @@ public class MiBand2Coordinator extends HuamiCoordinator { R.xml.devicesettings_donotdisturb_withauto, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_rotatewrist_cycleinfo, + R.xml.devicesettings_expose_hr_thirdparty, R.xml.devicesettings_pairingkey }; } From 8f4489a21ca751195d90d5d749a378fe33f4f36b Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 29 Aug 2019 08:32:29 +0200 Subject: [PATCH 022/154] Mi Band 4: Fix call notifcation not stopping when call gets answered or rejected on the phone This changes the way to how to stop call notification for all Huami device back to Mi Band 2 Also clean up unused code Fixes #1612 --- .../service/devices/huami/HuamiSupport.java | 73 ++----------------- .../miband2/Mi2TextNotificationStrategy.java | 6 ++ 2 files changed, 12 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 8accd9e62..c7560fcf2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -129,21 +129,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Version; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; @@ -331,23 +319,6 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { return this; } - /** - * Adds a custom notification to the given transaction builder - * @param vibrationProfile specifies how and how often the Band shall vibrate. - * @param simpleNotification - * @param flashTimes - * @param flashColour - * @param originalColour - * @param flashDuration - * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. - * @param builder - */ - private HuamiSupport sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { - getNotificationStrategy().sendCustomNotification(vibrationProfile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); - LOG.info("Sending notification to MiBand"); - return this; - } - public NotificationStrategy getNotificationStrategy() { String firmwareVersion = gbDevice.getFirmwareVersion(); if (firmwareVersion != null) { @@ -567,58 +538,26 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } } - protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) { + private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) { try { TransactionBuilder builder = performInitialized(task); Prefs prefs = GBApplication.getPrefs(); - int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); - int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes); profile.setAlertLevel(alertLevel); - int flashTimes = getPreferredFlashCount(notificationOrigin, prefs); - int flashColour = getPreferredFlashColour(notificationOrigin, prefs); - int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); - int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); + getNotificationStrategy().sendCustomNotification(profile, simpleNotification, 0, 0, 0, 0, extraAction, builder); - sendCustomNotification(profile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); - -// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); builder.queue(getQueue()); } catch (IOException ex) { - LOG.error("Unable to send notification to MI device", ex); + LOG.error("Unable to send notification to device", ex); } } - private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); - } - - private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); - } - - private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); - } - - private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); - } - - private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); - } - private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) { return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT)); } - private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) { - return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); - } - private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) { String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE); return VibrationProfile.getProfile(profileId, repeat); @@ -707,17 +646,17 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, HuamiService.ALERT_LEVEL_PHONE_CALL, abortAction); } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { telephoneRinging = false; - stopCurrentNotification(); + stopCurrentCallNotification(); } } - private void stopCurrentNotification() { + private void stopCurrentCallNotification() { try { TransactionBuilder builder = performInitialized("stop notification"); getNotificationStrategy().stopCurrentNotification(builder); builder.queue(getQueue()); } catch (IOException e) { - LOG.error("Error stopping notification"); + LOG.error("Error stopping call notification"); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java index 3abdad9e7..96291a9bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java @@ -86,4 +86,10 @@ public class Mi2TextNotificationStrategy extends Mi2NotificationStrategy { NewAlert alert = new NewAlert(category, 1, simpleNotification.getMessage()); profile.newAlert(builder, alert, OverflowStrategy.MAKE_MULTIPLE); } + + @Override + public void stopCurrentNotification(TransactionBuilder builder) { + BluetoothGattCharacteristic alert = getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT); + builder.write(alert, new byte[]{(byte) AlertCategory.IncomingCall.getId(), 0}); + } } From 66314f0e4b2b53fa4a27c6a0cf6b4e3d848e4ec3 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Fri, 30 Aug 2019 22:10:23 +0200 Subject: [PATCH 023/154] update issue template, thanks @IzzySoft --- .github/ISSUE_TEMPLATE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9c9c584ec..ec4f2168a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,6 +3,12 @@ - [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. - [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content) +### I got Gadgetbridge from: +* [ ] F-Droid +* [ ] I built it myself from source code (specify tag / commit) + +If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can. + #### Your issue is: *In case of a bug, do not forget to attach logs!* From 36010229ee975c8cbfac6bc3743d7af0b287e266 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Fri, 30 Aug 2019 22:13:10 +0200 Subject: [PATCH 024/154] fix the real bug report template --- .github/ISSUE_TEMPLATE.md | 25 ------------------------- .github/ISSUE_TEMPLATE/bug_report.md | 6 ++++++ 2 files changed, 6 insertions(+), 25 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ec4f2168a..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,25 +0,0 @@ -#### Before opening an issue please confirm the following: -- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question. -- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. -- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content) - -### I got Gadgetbridge from: -* [ ] F-Droid -* [ ] I built it myself from source code (specify tag / commit) - -If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can. - -#### Your issue is: -*In case of a bug, do not forget to attach logs!* - -#### Your wearable device is: - -*Please specify model and firmware version if possible* - -#### Your android version is: - -#### Your Gadgetbridge version is: - - - -*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.* diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 921bd9cc4..c027cfdfb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,12 @@ about: Create a report to help us improve - [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. - [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content) +### I got Gadgetbridge from: +* [ ] F-Droid +* [ ] I built it myself from source code (specify tag / commit) + +If you got it from Google Play, please note [that version](https://github.com/TaaviE/Gadgetbridge) is unofficial and not supported here; it's also often quite outdated. Please switch to one of the above versions if you can. + #### Your issue is: *If possible, please attach [logs](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* From 42e237feb263a4ddcaf5b8a076ac3aa69d058009 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 31 Aug 2019 21:53:16 +0200 Subject: [PATCH 025/154] Fix crash when entering notification filter notification filter settings Fixes #1614 Thanks @Nephiel for pointing that out --- app/src/main/res/layout-land/activity_notification_filter.xml | 4 ++-- app/src/main/res/layout/activity_notification_filter.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-land/activity_notification_filter.xml b/app/src/main/res/layout-land/activity_notification_filter.xml index 8c65b1efa..44e3e9ee2 100644 --- a/app/src/main/res/layout-land/activity_notification_filter.xml +++ b/app/src/main/res/layout-land/activity_notification_filter.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_notification_filter.xml b/app/src/main/res/layout/activity_notification_filter.xml index 9200b0595..1fde27508 100644 --- a/app/src/main/res/layout/activity_notification_filter.xml +++ b/app/src/main/res/layout/activity_notification_filter.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file From ce9eab8def9b392dcdbfa0bc3e71b5b1d62c2913 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 31 Aug 2019 22:14:50 +0200 Subject: [PATCH 026/154] Mi Band 4: really fix sending weather location Really fixes #1609 --- .../gadgetbridge/service/devices/huami/HuamiSupport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index c7560fcf2..3b323dafa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -1704,7 +1704,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { if (characteristicChunked != null) { - writeToChunked(builder, 4, buf.array()); + writeToChunked(builder, 1, buf.array()); } else { builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); } From 42049095a2b407a58fd2c2642e4afb0d053b08d2 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 31 Aug 2019 22:18:33 +0200 Subject: [PATCH 027/154] collect changes to far into CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eabecee4..f5624de59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ### Changelog +#### Next Version +* Mi Band 2/3/4, Amazfit Bip/Cor: dd setting to expose the HR sensor to 3rd party apps +* Mi Band 4: Really fix weather location not being updated on the Band +* Mi Band 4: Fix call notifcation not stopping when call gets answered or rejected on the phone +* Fix crash when entering notification filter notification filter settings + #### Version 0.36.0 * Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time * Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now) From b40c3ade85d0836b62a73f47d4814111fabe4c26 Mon Sep 17 00:00:00 2001 From: Nephiel Date: Wed, 31 Jul 2019 20:22:38 +0200 Subject: [PATCH 028/154] Amazfit Bip: Add emoji support when using custom font firmware --- .../devices/AbstractDeviceCoordinator.java | 3 ++ .../devices/DeviceCoordinator.java | 5 +++ .../devices/huami/HuamiConst.java | 1 + .../devices/huami/HuamiCoordinator.java | 5 +++ .../amazfitbip/AmazfitBipCoordinator.java | 5 +++ .../service/DeviceCommunicationService.java | 19 ++++++++++- .../gadgetbridge/util/StringUtils.java | 32 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ .../res/xml/devicesettings_amazfitbip.xml | 6 ++++ 9 files changed, 77 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 9f155c76e..d1466f573 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -153,6 +153,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { @Override public boolean supportsUnicodeEmojis() { return false; } + @Override + public boolean supportsCustomFont() { return false; } + @Override public int[] getSupportedDeviceSpecificSettings(GBDevice device) { return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index a9d7fd7e7..6b1049959 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -280,6 +280,11 @@ public interface DeviceCoordinator { */ boolean supportsUnicodeEmojis(); + /** + * Indicates whether the device supports using a custom font. + */ + boolean supportsCustomFont(); + /** * Indicates which device specific settings the device supports (not per device type or family, but unique per device). */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java index fe47cb21f..c4ccca2bf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiConst.java @@ -62,6 +62,7 @@ public class HuamiConst { public static final String PREF_LANGUAGE = "language"; public static final String PREF_DATEFORMAT = "dateformat"; public static final String PREF_EXPOSE_HR_THIRDPARTY = "expose_hr_thirdparty"; + public static final String PREF_USE_CUSTOM_FONT = "use_custom_font"; public static int toActivityKind(int rawType) { switch (rawType) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index d7bf1d353..2ec92bdf7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -197,6 +197,11 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator { return prefs.getStringSet(HuamiConst.PREF_DISPLAY_ITEMS, null); } + public static boolean getUseCustomFont(String deviceAddress) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress); + return prefs.getBoolean(HuamiConst.PREF_USE_CUSTOM_FONT, false); + } + public static boolean getGoalNotification() { Prefs prefs = GBApplication.getPrefs(); return prefs.getBoolean(MiBandConst.PREF_MI2_GOAL_NOTIFICATION, false); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java index 1971dd48c..73c82aded 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipCoordinator.java @@ -77,6 +77,11 @@ public class AmazfitBipCoordinator extends HuamiCoordinator { return true; } + @Override + public boolean supportsCustomFont() { + return true; + } + @Override public int[] getSupportedDeviceSpecificSettings(GBDevice device) { return new int[]{ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 152d457d1..4d3d439d0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver; @@ -80,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ADD_CALENDAREVENT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE; @@ -368,8 +370,23 @@ public class DeviceCommunicationService extends Service implements SharedPrefere if (text == null || text.length() == 0) return text; - if (!mCoordinator.supportsUnicodeEmojis()) + if (!mCoordinator.supportsUnicodeEmojis()) { + + // use custom font for emoji, if it is supported and enabled + if (mCoordinator.supportsCustomFont()) { + switch (mCoordinator.getDeviceType()) { + case AMAZFITBIP: + if (((HuamiCoordinator)mCoordinator).getUseCustomFont(mGBDevice.getAddress())) + return StringUtils.toCustomFont(text); + break; + // TODO: implement for Amazfit Cor + default: + break; + } + } + return EmojiConverter.convertUnicodeEmojiToAscii(text, getApplicationContext()); + } return text; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java index 007853deb..89dedbd24 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java @@ -96,4 +96,36 @@ public class StringUtils { } return ""; } + + /** + * @param str original text + * @return str with the emoticons in a format that can be displayed on an + * Amazfit Bip by using a custom font firmware with emoji support + */ + public static String toCustomFont(String str) { + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < str.length()) { + char charAt = str.charAt(i); + if (Character.isHighSurrogate(charAt)) { + int i2 = i + 1; + try { + int codePoint = Character.toCodePoint(charAt, str.charAt(i2)); + if (codePoint < 127744 || codePoint > 129510) { + sb.append(charAt); + } else { + sb.append(Character.toString((char) (codePoint - 83712))); + i = i2; + } + } catch (StringIndexOutOfBoundsException e) { + e.printStackTrace(); + sb.append(charAt); + } + } else { + sb.append(charAt); + } + i++; + } + return sb.toString(); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16289fdc8..96cc3b117 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,6 +167,8 @@ Reconnection attempts Allows other apps to access HR data in realtime while Gadgetbridge is connected 3rd party realtime HR access + Use custom font + Enable this if your device has a custom font firmware for emoji support Units diff --git a/app/src/main/res/xml/devicesettings_amazfitbip.xml b/app/src/main/res/xml/devicesettings_amazfitbip.xml index d2133e176..e469798bd 100644 --- a/app/src/main/res/xml/devicesettings_amazfitbip.xml +++ b/app/src/main/res/xml/devicesettings_amazfitbip.xml @@ -17,4 +17,10 @@ android:key="language" android:summary="%s" android:title="@string/pref_title_language" /> + From 957d4418597418d3be205f43e459e0e93628ace5 Mon Sep 17 00:00:00 2001 From: vanous Date: Tue, 13 Aug 2019 19:54:18 +0200 Subject: [PATCH 029/154] Add Status and Alarms widget Squashed commits from #1604 --- app/src/main/AndroidManifest.xml | 23 +- .../freeyourgadget/gadgetbridge/Widget.java | 226 ++++++++++++++++++ .../activities/WidgetAlarmsActivity.java | 127 ++++++++++ .../activities/charts/ActivityAnalysis.java | 6 +- .../gadgetbridge/model/DailyTotals.java | 155 ++++++++++++ .../operations/AbstractFetchOperation.java | 3 + app/src/main/res/drawable/widget_preview.png | Bin 0 -> 55080 bytes app/src/main/res/layout/widget.xml | 121 ++++++++++ .../layout/widget_alarms_activity_list.xml | 103 ++++++++ app/src/main/res/values/strings.xml | 21 +- app/src/main/res/xml/widget_info.xml | 14 ++ 11 files changed, 792 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java create mode 100644 app/src/main/res/drawable/widget_preview.png create mode 100644 app/src/main/res/layout/widget.xml create mode 100644 app/src/main/res/layout/widget_alarms_activity_list.xml create mode 100644 app/src/main/res/xml/widget_info.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e962b68c..dadfcfa39 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -445,7 +445,8 @@ android:resource="@xml/shared_paths" /> - + @@ -456,6 +457,26 @@ android:resource="@xml/sleep_alarm_widget_info" /> + + + + + + + + + + + + . */ +package nodomain.freeyourgadget.gadgetbridge; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.widget.RemoteViews; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; +import nodomain.freeyourgadget.gadgetbridge.activities.WidgetAlarmsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; +import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + + +public class Widget extends AppWidgetProvider { + public static final String WIDGET_CLICK = "nodomain.freeyourgadget.gadgetbridge.WidgetClick"; + public static final String NEW_DATA_ACTION = "nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"; + public static final String APPWIDGET_DELETED = "nodomain.freeyourgadget.gadgetbridge.APPWIDGET_DELETED"; + public static final String ACTION_DEVICE_CHANGED = "nodomain.freeyourgadget.gadgetbridge.gbdevice.action.device_changed"; + private static final Logger LOG = LoggerFactory.getLogger(Widget.class); + static BroadcastReceiver broadcastReceiver = null; + + private GBDevice getSelectedDevice() { + + Context context = GBApplication.getContext(); + + if (!(context instanceof GBApplication)) { + return null; + } + + GBApplication gbApp = (GBApplication) context; + + return gbApp.getDeviceManager().getSelectedDevice(); + } + + private float[] getSteps() { + Context context = GBApplication.getContext(); + Calendar day = GregorianCalendar.getInstance(); + + if (!(context instanceof GBApplication)) { + return new float[]{0, 0, 0}; + } + DailyTotals ds = new DailyTotals(); + return ds.getDailyTotalsForAllDevices(day); + } + + private String getHM(long value) { + return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES); + } + + private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + GBDevice device = getSelectedDevice(); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget); + + //onclick refresh + Intent intent = new Intent(context, Widget.class); + intent.setAction(WIDGET_CLICK); + PendingIntent refreshDataIntent = PendingIntent.getBroadcast( + context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + //views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, refreshDataIntent); + views.setOnClickPendingIntent(R.id.todaywidget_header_bar, refreshDataIntent); + + //open GB main window + Intent startMainIntent = new Intent(context, ControlCenterv2.class); + PendingIntent startMainPIntent = PendingIntent.getActivity(context, 0, startMainIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_header_icon, startMainPIntent); + + //alarms popup menu + Intent startAlarmListIntent = new Intent(context, WidgetAlarmsActivity.class); + PendingIntent startAlarmListPIntent = PendingIntent.getActivity(context, 0, startAlarmListIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_header_plus, startAlarmListPIntent); + + //charts, requires device + if (device != null) { + Intent startChartsIntent = new Intent(context, ChartsActivity.class); + startChartsIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + PendingIntent startChartsPIntent = PendingIntent.getActivity(context, 0, startChartsIntent, 0); + views.setOnClickPendingIntent(R.id.todaywidget_bottom_layout, startChartsPIntent); + } + + + float[] DailyTotals = getSteps(); + + views.setTextViewText(R.id.todaywidget_steps, context.getString(R.string.widget_steps_label, (int) DailyTotals[0])); + views.setTextViewText(R.id.todaywidget_sleep, context.getString(R.string.widget_sleep_label, getHM((long) DailyTotals[1]))); + + if (device != null) { + String status = String.format("%1s", device.getStateString()); + if (device.isConnected()) { + if (device.getBatteryLevel() > 1) { + status = String.format("Battery %1s%%", device.getBatteryLevel()); + } + } + + views.setTextViewText(R.id.todaywidget_device_status, status); + views.setTextViewText(R.id.todaywidget_device_name, device.getName()); + + } + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + public void refreshData() { + Context context = GBApplication.getContext(); + GBDevice device = getSelectedDevice(); + + if (device == null || !device.isInitialized()) { + GB.toast(context, + context.getString(R.string.device_not_connected), + Toast.LENGTH_SHORT, GB.ERROR); + GBApplication.deviceService().connect(); + GB.toast(context, + context.getString(R.string.connecting), + Toast.LENGTH_SHORT, GB.INFO); + + return; + } + GB.toast(context, + context.getString(R.string.busy_task_fetch_activity_data), + Toast.LENGTH_SHORT, GB.INFO); + + GBApplication.deviceService().onFetchRecordedData(RecordedDataTypes.TYPE_ACTIVITY); + } + + public void updateWidget() { + Context context = GBApplication.getContext(); + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + ComponentName thisAppWidget = new ComponentName(context.getPackageName(), Widget.class.getName()); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget); + + onUpdate(context, appWidgetManager, appWidgetIds); + + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + + @Override + public void onEnabled(Context context) { + if (this.broadcastReceiver == null) { + LOG.debug("gbwidget BROADCAST receiver initialized."); + this.broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LOG.debug("gbwidget BROADCAST, action" + intent.getAction()); + updateWidget(); + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(NEW_DATA_ACTION); + intentFilter.addAction(ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(context).registerReceiver(this.broadcastReceiver, intentFilter); + } + } + + @Override + public void onDisabled(Context context) { + + if (this.broadcastReceiver != null) { + LocalBroadcastManager.getInstance(context).unregisterReceiver(this.broadcastReceiver); + this.broadcastReceiver=null; + } + } + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + LOG.debug("gbwidget LOCAL onReceive, action: " + intent.getAction()); + //this handles widget re-connection after apk updates + if (WIDGET_CLICK.equals(intent.getAction())) { + if (this.broadcastReceiver == null) { + onEnabled(context); + } + refreshData(); + //updateWidget(); + } else if (APPWIDGET_DELETED.equals(intent.getAction())) { + onDisabled(context); + } + } + +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java new file mode 100644 index 000000000..a58924d2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/WidgetAlarmsActivity.java @@ -0,0 +1,127 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class WidgetAlarmsActivity extends Activity implements View.OnClickListener { + + TextView textView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context appContext = this.getApplicationContext(); + if (appContext instanceof GBApplication) { + GBApplication gbApp = (GBApplication) appContext; + GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice(); + if (selectedDevice == null || !selectedDevice.isInitialized()) { + GB.toast(this, + this.getString(R.string.not_connected), + Toast.LENGTH_LONG, GB.WARN); + + } else { + setContentView(R.layout.widget_alarms_activity_list); + int userSleepDuration = new ActivityUser().getSleepDuration(); + textView = findViewById(R.id.alarm5); + if (userSleepDuration > 0) { + textView.setText(String.format(this.getString(R.string.widget_alarm_target_hours), userSleepDuration)); + } else { + textView.setVisibility(View.GONE); + } + } + } + } + + @Override + public void onClick(View v) { + + switch (v.getId()) { + case R.id.alarm1: + setAlarm(5); + break; + case R.id.alarm2: + setAlarm(10); + break; + case R.id.alarm3: + setAlarm(20); + break; + case R.id.alarm4: + setAlarm(60); + break; + case R.id.alarm5: + setAlarm(0); + break; + default: + break; + } + //this is to prevent screen flashing during closing + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 150); + } + + public void setAlarm(int duration) { + // current timestamp + GregorianCalendar calendar = new GregorianCalendar(); + if (duration > 0) { + calendar.add(Calendar.MINUTE, duration); + } else { + int userSleepDuration = new ActivityUser().getSleepDuration(); + // add preferred sleep duration + if (userSleepDuration > 0) { + calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration); + } else { // probably testing + calendar.add(Calendar.MINUTE, 1); + } + } + + // overwrite the first alarm and activate it, without + + Context appContext = this.getApplicationContext(); + if (appContext instanceof GBApplication) { + GBApplication gbApp = (GBApplication) appContext; + GBDevice selectedDevice = gbApp.getDeviceManager().getSelectedDevice(); + if (selectedDevice == null || !selectedDevice.isInitialized()) { + GB.toast(this, + this.getString(R.string.appwidget_not_connected), + Toast.LENGTH_LONG, GB.WARN); + return; + } + } + + int hours = calendar.get(Calendar.HOUR_OF_DAY); + int minutes = calendar.get(Calendar.MINUTE); + + GB.toast(this, + this.getString(R.string.appwidget_setting_alarm, hours, minutes), + Toast.LENGTH_SHORT, GB.INFO); + + Alarm alarm = AlarmUtils.createSingleShot(0, true, calendar); + ArrayList alarms = new ArrayList<>(1); + alarms.add(alarm); + GBApplication.deviceService().onSetAlarms(alarms); + + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index 155dcf7b2..25e7d342e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -28,15 +28,15 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -class ActivityAnalysis { - private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class); +public class ActivityAnalysis { + public static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class); // store raw steps and duration protected HashMap stats = new HashMap(); // max speed determined from samples private int maxSpeed = 0; - ActivityAmounts calculateActivityAmounts(List samples) { + public ActivityAmounts calculateActivityAmounts(List samples) { ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP); ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP); ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java new file mode 100644 index 000000000..bcd8ff73e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -0,0 +1,155 @@ +/* Copyright (C) 2017-2019 Carsten Pfeiffer, Daniele Gobbetti + + 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 + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.model; + +import android.content.Context; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityAnalysis; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + + + +public class DailyTotals { + Logger LOG = LoggerFactory.getLogger(DailyTotals.class); + + + public float[] getDailyTotalsForAllDevices(Calendar day) { + Context context = GBApplication.getContext(); + //get today's steps for all devices in GB + float all_steps = 0; + float all_sleep = 0; + + + if (context instanceof GBApplication) { + GBApplication gbApp = (GBApplication) context; + List devices = gbApp.getDeviceManager().getDevices(); + for (GBDevice device : devices) { + float[] all_daily = getDailyTotalsForDevice(device, day); + all_steps += all_daily[0]; + all_sleep += all_daily[1] + all_daily[2]; + } + } + LOG.debug("gbwidget daily totals, all steps:" + all_steps); + LOG.debug("gbwidget daily totals, all sleep:" + all_sleep); + return new float[]{all_steps, all_sleep}; + } + + + public float[] getDailyTotalsForDevice(GBDevice device, Calendar day + ) { + + try (DBHandler handler = GBApplication.acquireDB()) { + ActivityAnalysis analysis = new ActivityAnalysis(); + ActivityAmounts amountsSteps = null; + ActivityAmounts amountsSleep = null; + + amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); + amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device)); + + float[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep); + float Steps = getTotalsStepsForActivityAmounts(amountsSteps); + + return new float[]{Steps, Sleep[0], Sleep[1]}; + + } catch (Exception e) { + + GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e); + return new float[]{0, 0, 0}; + } + } + + private float[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) { + long totalSecondsDeepSleep = 0; + long totalSecondsLightSleep = 0; + for (ActivityAmount amount : activityAmounts.getAmounts()) { + if (amount.getActivityKind() == ActivityKind.TYPE_DEEP_SLEEP) { + totalSecondsDeepSleep += amount.getTotalSeconds(); + } else if (amount.getActivityKind() == ActivityKind.TYPE_LIGHT_SLEEP) { + totalSecondsLightSleep += amount.getTotalSeconds(); + } + } + int totalMinutesDeepSleep = (int) (totalSecondsDeepSleep / 60); + int totalMinutesLightSleep = (int) (totalSecondsLightSleep / 60); + return new float[]{totalMinutesDeepSleep, totalMinutesLightSleep}; + } + + + private float getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) { + long totalSteps = 0; + float totalValue = 0; + + for (ActivityAmount amount : activityAmounts.getAmounts()) { + totalSteps += amount.getTotalSteps(); + } + + float[] totalValues = new float[]{totalSteps}; + + for (int i = 0; i < totalValues.length; i++) { + float value = totalValues[i]; + totalValue += value; + } + return totalValue; + } + + + private List getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) { + int startTs; + int endTs; + + day = (Calendar) day.clone(); // do not modify the caller's argument + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.add(Calendar.HOUR, offsetHours); + + startTs = (int) (day.getTimeInMillis() / 1000); + endTs = startTs + 24 * 60 * 60 - 1; + + return getSamples(db, device, startTs, endTs); + } + + + protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + return getAllSamples(db, device, tsFrom, tsTo); + } + + + protected SampleProvider getProvider(DBHandler db, GBDevice device) { + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + return coordinator.getSampleProvider(device, db.getDaoSession()); + } + + + protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider provider = getProvider(db, device); + return provider.getAllActivitySamples(tsFrom, tsTo); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java index 915db7cd7..1c9d7691b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java @@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; import android.content.SharedPreferences; import android.widget.Toast; @@ -126,6 +127,8 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation { GB.updateTransferNotification(null, "", false, 100, getContext()); operationFinished(); unsetBusy(); + Intent intent = new Intent("nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"); + getContext().sendBroadcast(intent); } /** diff --git a/app/src/main/res/drawable/widget_preview.png b/app/src/main/res/drawable/widget_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad18604198964b29419757963de0f401aa837ee GIT binary patch literal 55080 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/widget.xml b/app/src/main/res/layout/widget.xml new file mode 100644 index 000000000..e6aca5181 --- /dev/null +++ b/app/src/main/res/layout/widget.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_alarms_activity_list.xml b/app/src/main/res/layout/widget_alarms_activity_list.xml new file mode 100644 index 000000000..c646f189a --- /dev/null +++ b/app/src/main/res/layout/widget_alarms_activity_list.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96cc3b117..47fd8a23b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -527,10 +527,7 @@ Authenticating Authentication required - Zzz - Add widget Preferred sleep duration in hours - Setting alarm for %1$02d:%2$02d Hardware revision: %1$s Firmware version: %1$s Error creating directory for log files: %1$s @@ -715,5 +712,23 @@ Filter Mode Mode Configuration Save Configuration + Not connected, alarm not set. + Zzz + Add widget + Setting alarm for %1$02d:%2$02d + Sleep Alarm + + Steps: %1$02d + Sleep: %1$s + Status and Alarms + Set alarm after: + 5 minutes + 10 minutes + 20 minutes + 1 hour + Icon + %d hours + + diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml new file mode 100644 index 000000000..9bc0b9b98 --- /dev/null +++ b/app/src/main/res/xml/widget_info.xml @@ -0,0 +1,14 @@ + + + + From 8eb494ab85d2124cb82f520fc31dfec9777f1b27 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 1 Sep 2019 22:09:09 +0200 Subject: [PATCH 030/154] Widget: some cleanups - use LocalBroadcastManager when broadcasting new data event - use constants for actions everywhere and move them --- .../gadgetbridge/GBApplication.java | 1 + .../freeyourgadget/gadgetbridge/Widget.java | 21 +++++++++---------- .../operations/AbstractFetchOperation.java | 6 ++++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 94a980ce5..8c3e8dfff 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -112,6 +112,7 @@ public class GBApplication extends Application { public static final String ACTION_QUIT = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit"; public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change"; + public static final String ACTION_NEW_DATA = "nodomain.freeyourgadget.gadgetbridge.action.new_data"; private static GBApplication app; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java index f9cf13dc1..a9a0a42f4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java @@ -42,15 +42,14 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class Widget extends AppWidgetProvider { public static final String WIDGET_CLICK = "nodomain.freeyourgadget.gadgetbridge.WidgetClick"; - public static final String NEW_DATA_ACTION = "nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"; public static final String APPWIDGET_DELETED = "nodomain.freeyourgadget.gadgetbridge.APPWIDGET_DELETED"; - public static final String ACTION_DEVICE_CHANGED = "nodomain.freeyourgadget.gadgetbridge.gbdevice.action.device_changed"; private static final Logger LOG = LoggerFactory.getLogger(Widget.class); static BroadcastReceiver broadcastReceiver = null; @@ -181,9 +180,9 @@ public class Widget extends AppWidgetProvider { @Override public void onEnabled(Context context) { - if (this.broadcastReceiver == null) { + if (broadcastReceiver == null) { LOG.debug("gbwidget BROADCAST receiver initialized."); - this.broadcastReceiver = new BroadcastReceiver() { + broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { LOG.debug("gbwidget BROADCAST, action" + intent.getAction()); @@ -191,18 +190,18 @@ public class Widget extends AppWidgetProvider { } }; IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(NEW_DATA_ACTION); - intentFilter.addAction(ACTION_DEVICE_CHANGED); - LocalBroadcastManager.getInstance(context).registerReceiver(this.broadcastReceiver, intentFilter); + intentFilter.addAction(GBApplication.ACTION_NEW_DATA); + intentFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter); } } @Override public void onDisabled(Context context) { - if (this.broadcastReceiver != null) { - LocalBroadcastManager.getInstance(context).unregisterReceiver(this.broadcastReceiver); - this.broadcastReceiver=null; + if (broadcastReceiver != null) { + AndroidUtils.safeUnregisterBroadcastReceiver(context,broadcastReceiver); + broadcastReceiver = null; } } @@ -212,7 +211,7 @@ public class Widget extends AppWidgetProvider { LOG.debug("gbwidget LOCAL onReceive, action: " + intent.getAction()); //this handles widget re-connection after apk updates if (WIDGET_CLICK.equals(intent.getAction())) { - if (this.broadcastReceiver == null) { + if (broadcastReceiver == null) { onEnabled(context); } refreshData(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java index 1c9d7691b..1da7f98b0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java @@ -25,6 +25,7 @@ import android.widget.Toast; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -127,8 +128,9 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation { GB.updateTransferNotification(null, "", false, 100, getContext()); operationFinished(); unsetBusy(); - Intent intent = new Intent("nodomain.freeyourgadget.gadgetbridge.NewDataTrigger"); - getContext().sendBroadcast(intent); + + Intent intent = new Intent(GBApplication.ACTION_NEW_DATA); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } /** From 3389fcdfdd64b69aef27911c0f9f08a88550e684 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 1 Sep 2019 22:35:02 +0200 Subject: [PATCH 031/154] Widget: Fix null pointer exception when a device which has no activity database is used in Gadgetbridge For example a vibratissimo :D --- .../gadgetbridge/model/DailyTotals.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java index bcd8ff73e..c92773351 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -36,9 +36,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; - public class DailyTotals { - Logger LOG = LoggerFactory.getLogger(DailyTotals.class); + private static final Logger LOG = LoggerFactory.getLogger(DailyTotals.class); public float[] getDailyTotalsForAllDevices(Calendar day) { @@ -52,6 +51,10 @@ public class DailyTotals { GBApplication gbApp = (GBApplication) context; List devices = gbApp.getDeviceManager().getDevices(); for (GBDevice device : devices) { + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + if (!coordinator.supportsActivityDataFetching()) { + continue; + } float[] all_daily = getDailyTotalsForDevice(device, day); all_steps += all_daily[0]; all_sleep += all_daily[1] + all_daily[2]; @@ -63,13 +66,12 @@ public class DailyTotals { } - public float[] getDailyTotalsForDevice(GBDevice device, Calendar day - ) { + public float[] getDailyTotalsForDevice(GBDevice device, Calendar day) { try (DBHandler handler = GBApplication.acquireDB()) { ActivityAnalysis analysis = new ActivityAnalysis(); - ActivityAmounts amountsSteps = null; - ActivityAmounts amountsSleep = null; + ActivityAmounts amountsSteps; + ActivityAmounts amountsSleep; amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device)); From 4780c26dd893ff6a22fd527028c2edc307b74087 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 1 Sep 2019 22:48:52 +0200 Subject: [PATCH 032/154] Widget: change float to int where appropriate, remove code that did nothing from getTotalsStepsForActivityAmounts() --- .../freeyourgadget/gadgetbridge/Widget.java | 6 +-- .../gadgetbridge/model/DailyTotals.java | 38 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java index a9a0a42f4..bed0d9a3f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java @@ -66,12 +66,12 @@ public class Widget extends AppWidgetProvider { return gbApp.getDeviceManager().getSelectedDevice(); } - private float[] getSteps() { + private int[] getSteps() { Context context = GBApplication.getContext(); Calendar day = GregorianCalendar.getInstance(); if (!(context instanceof GBApplication)) { - return new float[]{0, 0, 0}; + return new int[]{0, 0, 0}; } DailyTotals ds = new DailyTotals(); return ds.getDailyTotalsForAllDevices(day); @@ -114,7 +114,7 @@ public class Widget extends AppWidgetProvider { } - float[] DailyTotals = getSteps(); + int[] DailyTotals = getSteps(); views.setTextViewText(R.id.todaywidget_steps, context.getString(R.string.widget_steps_label, (int) DailyTotals[0])); views.setTextViewText(R.id.todaywidget_sleep, context.getString(R.string.widget_sleep_label, getHM((long) DailyTotals[1]))); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java index c92773351..703e64736 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -40,11 +40,11 @@ public class DailyTotals { private static final Logger LOG = LoggerFactory.getLogger(DailyTotals.class); - public float[] getDailyTotalsForAllDevices(Calendar day) { + public int[] getDailyTotalsForAllDevices(Calendar day) { Context context = GBApplication.getContext(); //get today's steps for all devices in GB - float all_steps = 0; - float all_sleep = 0; + int all_steps = 0; + int all_sleep = 0; if (context instanceof GBApplication) { @@ -55,18 +55,18 @@ public class DailyTotals { if (!coordinator.supportsActivityDataFetching()) { continue; } - float[] all_daily = getDailyTotalsForDevice(device, day); + int[] all_daily = getDailyTotalsForDevice(device, day); all_steps += all_daily[0]; all_sleep += all_daily[1] + all_daily[2]; } } LOG.debug("gbwidget daily totals, all steps:" + all_steps); LOG.debug("gbwidget daily totals, all sleep:" + all_sleep); - return new float[]{all_steps, all_sleep}; + return new int[]{all_steps, all_sleep}; } - public float[] getDailyTotalsForDevice(GBDevice device, Calendar day) { + public int[] getDailyTotalsForDevice(GBDevice device, Calendar day) { try (DBHandler handler = GBApplication.acquireDB()) { ActivityAnalysis analysis = new ActivityAnalysis(); @@ -76,19 +76,19 @@ public class DailyTotals { amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device)); amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device)); - float[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep); - float Steps = getTotalsStepsForActivityAmounts(amountsSteps); + int[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep); + int Steps = getTotalsStepsForActivityAmounts(amountsSteps); - return new float[]{Steps, Sleep[0], Sleep[1]}; + return new int[]{Steps, Sleep[0], Sleep[1]}; } catch (Exception e) { GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e); - return new float[]{0, 0, 0}; + return new int[]{0, 0, 0}; } } - private float[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) { + private int[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) { long totalSecondsDeepSleep = 0; long totalSecondsLightSleep = 0; for (ActivityAmount amount : activityAmounts.getAmounts()) { @@ -100,25 +100,17 @@ public class DailyTotals { } int totalMinutesDeepSleep = (int) (totalSecondsDeepSleep / 60); int totalMinutesLightSleep = (int) (totalSecondsLightSleep / 60); - return new float[]{totalMinutesDeepSleep, totalMinutesLightSleep}; + return new int[]{totalMinutesDeepSleep, totalMinutesLightSleep}; } - private float getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) { - long totalSteps = 0; - float totalValue = 0; + private int getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) { + int totalSteps = 0; for (ActivityAmount amount : activityAmounts.getAmounts()) { totalSteps += amount.getTotalSteps(); } - - float[] totalValues = new float[]{totalSteps}; - - for (int i = 0; i < totalValues.length; i++) { - float value = totalValues[i]; - totalValue += value; - } - return totalValue; + return totalSteps; } From 503fe854eb175674a0a926226805a7a1555de49c Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 2 Sep 2019 23:06:39 +0200 Subject: [PATCH 033/154] Add test button, @stringify strings, add headers --- .../activities/DbManagementActivity.java | 30 +++++++ .../res/layout/activity_db_management.xml | 78 ++++++++++++++++--- app/src/main/res/values/strings.xml | 12 ++- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java index 66b022626..3a0765db2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DbManagementActivity.java @@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities; import android.app.AlertDialog; import android.content.DialogInterface; +import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteOpenHelper; import android.os.Bundle; @@ -42,10 +43,13 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class DbManagementActivity extends AbstractGBActivity { @@ -97,6 +101,32 @@ public class DbManagementActivity extends AbstractGBActivity { } }); + Prefs prefs = GBApplication.getPrefs(); + boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false); + Integer autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0); + String autoExportLocation = prefs.getString(GBPrefs.AUTO_EXPORT_LOCATION, ""); + + int testExportVisibility = (autoExportInterval > 0 && autoExportEnabled) ? View.VISIBLE : View.GONE; + + TextView autoExportLocation_label = findViewById(R.id.autoExportLocation_label); + autoExportLocation_label.setVisibility(testExportVisibility); + + TextView autoExportLocation_intro = findViewById(R.id.autoExportLocation_intro); + autoExportLocation_intro.setVisibility(testExportVisibility); + + TextView autoExportLocationview = findViewById(R.id.autoExportLocationview); + autoExportLocationview.setVisibility(testExportVisibility); + autoExportLocationview.setText(autoExportLocation); + + Button testExportDBButton = findViewById(R.id.testExportDBButton); + testExportDBButton.setVisibility(testExportVisibility); + testExportDBButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sendBroadcast(new Intent(getApplicationContext(), PeriodicExporter.class)); + } + }); + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); } diff --git a/app/src/main/res/layout/activity_db_management.xml b/app/src/main/res/layout/activity_db_management.xml index c61aa993f..912a642c9 100644 --- a/app/src/main/res/layout/activity_db_management.xml +++ b/app/src/main/res/layout/activity_db_management.xml @@ -1,4 +1,5 @@ - + +