1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 04:46:51 +01:00

Merge branch 'master' of github.com:Freeyourgadget/Gadgetbridge into fossil-q-hybrid

This commit is contained in:
dakhnod 2019-09-25 00:22:57 +02:00
commit b20963348d
167 changed files with 7447 additions and 1811 deletions

View File

@ -1,19 +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)
#### 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.*

View File

@ -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.*

View File

@ -1,7 +1,40 @@
### Changelog
#### Version 0.36.2
* Amazfit Bip: Untested support for Lite variant
* Force Lineage OS to ask for permission when Trust is used to fix non-working incoming calls
* Charts: List multiple sleep sessions per day
#### Version 0.36.1
* Mi Band 2/3/4, Amazfit Bip/Cor: Add 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
* Amazfit Bip/Cor: Support for custom emoji font
* ZeTime: Enable emoji support
* ZeTime: Make watch language the same as the phone language by default
* New status and alarms widget
* Fix crash when entering notification filter settings
* Make diagram settings accessible from charts activity
* Add option to hide the floating plus button in the main activity
* Fix a potential crash on Android 4.4 KitKat
#### 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)
* 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
#### Version 0.35.2
* Mi Band 1/2: Crash when updating firemare while phone is set to Spanish
* 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)
* Mi Band 4: Support setting date format (for built-in watchfaces)
* Amazfit Cor 2: Try to fix empty menu on device

View File

@ -1,5 +1,5 @@
apply plugin: "com.android.application"
apply plugin: "findbugs"
apply plugin: "com.github.spotbugs"
apply plugin: "pmd"
def ABORT_ON_CHECK_FAILURE = false
@ -25,8 +25,8 @@ android {
targetSdkVersion 27
// Note: always bump BOTH versionCode and versionName!
versionName "0.35.2"
versionCode 154
versionName "0.36.2"
versionCode 157
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -66,8 +66,9 @@ dependencies {
testImplementation "org.robolectric:robolectric:4.2.1"
testImplementation "com.google.code.gson:gson:2.8.5"
implementation "androidx.appcompat:appcompat:1.0.2"
implementation "androidx.preference:preference:1.1.0-alpha05"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
@ -78,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"
@ -107,7 +108,7 @@ gradle.beforeProject {
preBuild.dependsOn(":GBDaoGenerator:genSources")
}
check.dependsOn "findbugs", "pmd", "lint"
check.dependsOn "spotbugsMain", "pmd", "lint"
task pmd(type: Pmd) {
ruleSetFiles = files("${project.rootDir}/config/pmd/pmd-ruleset.xml")
@ -150,22 +151,32 @@ task pmd(type: Pmd) {
}
}
task findbugs(type: FindBugs) {
// this is just for spotbugs to let the plugin create the task
sourceSets {
main {
java.srcDirs = []
}
}
spotbugs {
toolVersion = "3.1.12"
ignoreFailures = !ABORT_ON_CHECK_FAILURE
effort = "default"
reportLevel = "medium"
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
source = fileTree('src/main/java')
classes = files("${project.rootDir}/app/build/intermediates/javac/debug/classes")
excludeFilter = new File("${project.rootDir}/config/findbugs/findbugs-filter.xml")
classes = files("${project.rootDir}/app/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes")
source = fileTree("src/main/java/")
classpath = files()
reports {
xml.enabled = false
html.enabled = true
xml {
destination file ("$project.buildDir/reports/findbugs/findbugs-output.xml")
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
}
html {
destination file ("$project.buildDir/reports/findbugs/findbugs-output.html")
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.html")
}
}
}

View File

@ -24,6 +24,8 @@
<uses-permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="cyanogenmod.permission.READ_WEATHER" />
<uses-permission android:name="lineageos.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="lineageos.permission.READ_WEATHER" />
<uses-permission android:name="org.omnirom.omnijaws.READ_WEATHER" />
<uses-feature
@ -58,6 +60,10 @@
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.charts.ChartsPreferencesActivity"
android:label="@string/activity_prefs_charts"
android:parentActivityName=".activities.charts.ChartsPreferencesActivity" />
<activity
android:name=".devices.miband.MiBandPreferencesActivity"
android:label="@string/preferences_miband_settings"
@ -443,7 +449,8 @@
android:resource="@xml/shared_paths" />
</provider>
<receiver android:name=".SleepAlarmWidget">
<receiver android:name=".SleepAlarmWidget"
android:label="@string/appwidget_sleep_alarm_widget_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
@ -454,6 +461,26 @@
android:resource="@xml/sleep_alarm_widget_info" />
</receiver>
<receiver
android:name=".Widget"
android:label="@string/widget_listing_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.WidgetClick" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<activity
android:name=".activities.WidgetAlarmsActivity"
android:launchMode="singleInstance"
android:theme="@style/Theme.AppCompat.Light.Dialog"
android:excludeFromRecents="true"/>
<activity
android:launchMode="singleTask"
android:allowTaskReparenting="true"

View File

@ -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);
}

View File

@ -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> weatherLocation);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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.
*
* <p>On incoming parcel (to be unmarshalled):
*
* <pre class="prettyprint">
* ParcelInfo incomingParcelInfo = Concierge.receiveParcel(incomingParcel);
* int parcelableVersion = incomingParcelInfo.getParcelVersion();
*
* // Do unmarshalling steps here iterating over every plausible version
*
* // Complete the process
* incomingParcelInfo.complete();
* </pre>
*
* <p>On outgoing parcel (to be marshalled):
*
* <pre class="prettyprint">
* ParcelInfo outgoingParcelInfo = Concierge.prepareParcel(incomingParcel);
*
* // Do marshalling steps here iterating over every plausible version
*
* // Complete the process
* outgoingParcelInfo.complete();
* </pre>
*/
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);
}
}
}
}

View File

@ -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
* <P>Type: TEXT</P>
*/
public static final String CURRENT_CITY = "city";
/**
* A Valid {@link WeatherCode}
* <P>Type: INTEGER</P>
*/
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
* <P>Type: TEXT</P>
*/
public static final String CURRENT_CONDITION = "condition";
/**
* The current weather temperature
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_TEMPERATURE = "temperature";
/**
* The unit in which current temperature is reported
* <P>Type: INTEGER</P>
* Can be one of the following:
* <ul>
* <li>{@link TempUnit#CELSIUS}</li>
* <li>{@link TempUnit#FAHRENHEIT}</li>
* </ul>
*/
public static final String CURRENT_TEMPERATURE_UNIT = "temperature_unit";
/**
* The current weather humidity
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_HUMIDITY = "humidity";
/**
* The current wind direction (in degrees)
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_WIND_DIRECTION = "wind_direction";
/**
* The current wind speed
* <P>Type: DOUBLE</P>
*/
public static final String CURRENT_WIND_SPEED = "wind_speed";
/**
* The unit in which the wind speed is reported
* <P>Type: INTEGER</P>
* Can be one of the following:
* <ul>
* <li>{@link WindSpeedUnit#KPH}</li>
* <li>{@link WindSpeedUnit#MPH}</li>
* </ul>
*/
public static final String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit";
/**
* The timestamp when this weather was reported
* <P>Type: LONG</P>
*/
public static final String CURRENT_TIMESTAMP = "timestamp";
/**
* Today's high temperature.
* <p>Type: DOUBLE</p>
*/
public static final String TODAYS_HIGH_TEMPERATURE = "todays_high";
/**
* Today's low temperature.
* <p>Type: DOUBLE</p>
*/
public static final String TODAYS_LOW_TEMPERATURE = "todays_low";
/**
* The forecasted low temperature
* <P>Type: DOUBLE</P>
*/
public static final String FORECAST_LOW = "forecast_low";
/**
* The forecasted high temperature
* <P>Type: DOUBLE</P>
*/
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
* <P>Type: TEXT</P>
*/
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;
}
}
}

View File

@ -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<RequestInfo,WeatherUpdateRequestListener> mWeatherUpdateRequestListeners
= Collections.synchronizedMap(new HashMap<RequestInfo,WeatherUpdateRequestListener>());
private Map<RequestInfo,LookupCityRequestListener> mLookupNameRequestListeners
= Collections.synchronizedMap(new HashMap<RequestInfo,LookupCityRequestListener>());
private Handler mHandler;
private Set<WeatherServiceProviderChangeListener> 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<WeatherServiceProviderChangeListener> 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<WeatherLocation> 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<WeatherLocation> 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
* <p>The user removed the active weather service provider from the system </p>
* <p>The active weather provider was disabled.</p>
*
* @param providerLabel The label as declared on the weather service provider manifest
*/
void onWeatherServiceProviderChanged(String providerLabel);
}
}

View File

@ -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:
* <ul>
* {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#CELSIUS}
* {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#FAHRENHEIT}
* </ul>
* 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<RequestInfo> CREATOR = new Creator<RequestInfo>() {
@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;
}
}

View File

@ -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<DayForecast> 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<DayForecast> 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<DayForecast> 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<DayForecast> 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<WeatherInfo> CREATOR =
new Parcelable.Creator<WeatherInfo>() {
@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<DayForecast> CREATOR =
new Parcelable.Creator<DayForecast>() {
@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;
}
}

View File

@ -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<WeatherLocation> CREATOR = new Creator<WeatherLocation>() {
@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;
}
}

View File

@ -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&deg;F or XX&deg;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;
}
}
}

View File

@ -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.
* <ul>
* <li>{@link lineageos.weather.LineageWeatherManager.RequestStatus#SUBMITTED_TOO_SOON}</li>
* <li>{@link lineageos.weather.LineageWeatherManager.RequestStatus#ALREADY_IN_PROGRESS}</li>
* </ul>
* 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;
}
}
}

View File

@ -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<WeatherLocation> 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<ServiceRequestResult> CREATOR
= new Creator<ServiceRequestResult>() {
@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<WeatherLocation> 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<WeatherLocation> 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<WeatherLocation> 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;
}
}

View File

@ -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<ServiceRequest> mWeakRequestsSet
= Collections.newSetFromMap(new WeakHashMap<ServiceRequest, Boolean>());
/**
* 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 <code>&lt;weather-provider-service&gt;</code>
* 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
* <b>has marked the request as cancelled</b> 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);
}

View File

@ -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;
@ -631,8 +632,9 @@ public class GBApplication extends Application {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences.Editor deviceSharedPrefsEdit = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier()).edit();
if (sharedPrefs != null) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
String preferenceKey = dbDevice.getIdentifier() + "_lastSportsActivityTimeMillis";
long lastSportsActivityTimeMillis = sharedPrefs.getLong(preferenceKey, 0);
if (lastSportsActivityTimeMillis != 0) {
@ -707,9 +709,8 @@ public class GBApplication extends Application {
if (newLanguage != null) {
deviceSharedPrefsEdit.putString("language", newLanguage);
}
deviceSharedPrefsEdit.apply();
}
deviceSharedPrefsEdit.apply();
}
editor.remove("amazfitbip_language");
editor.remove("bip_display_items");

View File

@ -0,0 +1,225 @@
/* Copyright (C) 2016-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer
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 <http://www.gnu.org/licenses/>. */
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.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 APPWIDGET_DELETED = "nodomain.freeyourgadget.gadgetbridge.APPWIDGET_DELETED";
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 int[] getSteps() {
Context context = GBApplication.getContext();
Calendar day = GregorianCalendar.getInstance();
if (!(context instanceof GBApplication)) {
return new int[]{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);
}
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])));
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 (broadcastReceiver == null) {
LOG.debug("gbwidget BROADCAST receiver initialized.");
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(GBApplication.ACTION_NEW_DATA);
intentFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter);
}
}
@Override
public void onDisabled(Context context) {
if (broadcastReceiver != null) {
AndroidUtils.safeUnregisterBroadcastReceiver(context,broadcastReceiver);
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 (broadcastReceiver == null) {
onEnabled(context);
}
refreshData();
//updateWidget();
} else if (APPWIDGET_DELETED.equals(intent.getAction())) {
onDisabled(context);
}
}
}

View File

@ -36,6 +36,10 @@ import android.widget.DatePicker;
import android.widget.ListView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.File;
@ -45,9 +49,6 @@ import java.util.Calendar;
import java.util.List;
import java.util.Objects;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
@ -240,10 +241,10 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
date.set(year, monthOfYear, dayOfMonth);
long timestamp = date.getTimeInMillis() - 1000;
SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit();
editor.remove(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis", timestamp);
editor.commit();
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
editor.putLong("lastSportsActivityTimeMillis", timestamp);
editor.apply();
}
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
}

View File

@ -27,17 +27,11 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.view.MenuItem;
import android.view.View;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
@ -50,6 +44,15 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.navigation.NavigationView;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -73,9 +76,14 @@ public class ControlCenterv2 extends AppCompatActivity
private GBDeviceAdapterv2 mGBDeviceAdapter;
private RecyclerView deviceListView;
private FloatingActionButton fab;
private boolean isLanguageInvalid = false;
public static final int MENU_REFRESH_CODE=1;
private static PhoneStateListener fakeStateListener;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -103,14 +111,6 @@ public class ControlCenterv2 extends AppCompatActivity
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchDiscoveryActivity();
}
});
DrawerLayout drawer = findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
@ -132,6 +132,16 @@ public class ControlCenterv2 extends AppCompatActivity
deviceListView.setAdapter(this.mGBDeviceAdapter);
fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
launchDiscoveryActivity();
}
});
showFabIfNeccessary();
/* uncomment to enable fixed-swipe to reveal more actions
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
@ -230,6 +240,15 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == MENU_REFRESH_CODE) {
showFabIfNeccessary();
}
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
@ -239,7 +258,7 @@ public class ControlCenterv2 extends AppCompatActivity
switch (item.getItemId()) {
case R.id.action_settings:
Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivity(settingsIntent);
startActivityForResult(settingsIntent, MENU_REFRESH_CODE);
return true;
case R.id.action_debug:
Intent debugIntent = new Intent(this, DebugActivity.class);
@ -253,6 +272,9 @@ public class ControlCenterv2 extends AppCompatActivity
Intent blIntent = new Intent(this, AppBlacklistActivity.class);
startActivity(blIntent);
return true;
case R.id.device_action_discover:
launchDiscoveryActivity();
return true;
case R.id.action_quit:
GBApplication.quit();
return true;
@ -277,7 +299,8 @@ public class ControlCenterv2 extends AppCompatActivity
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
"}";
return new ChangeLog(this, css);
}
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivity.class));
}
@ -286,6 +309,18 @@ public class ControlCenterv2 extends AppCompatActivity
mGBDeviceAdapter.notifyDataSetChanged();
}
private void showFabIfNeccessary() {
if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) {
fab.show();
} else {
if (deviceListView.getChildCount() < 1) {
fab.show();
} else {
fab.hide();
}
}
}
@TargetApi(Build.VERSION_CODES.M)
private void checkAndRequestPermissions() {
List<String> wantedPermissions = new ArrayList<>();
@ -321,7 +356,15 @@ public class ControlCenterv2 extends AppCompatActivity
}
if (!wantedPermissions.isEmpty())
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
// HACK: On Lineage we have to do this so that the permission dialog pops up
if (fakeStateListener == null) {
fakeStateListener = new PhoneStateListener();
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_CALL_STATE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_NONE);
}
}
public void setLanguage(Locale language, boolean invalidateLanguage) {

View File

@ -17,12 +17,18 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
@ -42,10 +48,14 @@ 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.AndroidUtils;
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,9 +107,68 @@ public class DbManagementActivity extends AbstractGBActivity {
}
});
Prefs prefs = GBApplication.getPrefs();
boolean autoExportEnabled = prefs.getBoolean(GBPrefs.AUTO_EXPORT_ENABLED, false);
int autoExportInterval = prefs.getInt(GBPrefs.AUTO_EXPORT_INTERVAL, 0);
//returns an ugly content://...
//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 autoExportLocation_path = findViewById(R.id.autoExportLocation_path);
autoExportLocation_path.setVisibility(testExportVisibility);
autoExportLocation_path.setText(getAutoExportLocationSummary());
final Context context = getApplicationContext();
Button testExportDBButton = findViewById(R.id.testExportDBButton);
testExportDBButton.setVisibility(testExportVisibility);
testExportDBButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendBroadcast(new Intent(context, PeriodicExporter.class));
GB.toast(context,
context.getString(R.string.activity_DB_test_export_message),
Toast.LENGTH_SHORT, GB.INFO);
}
});
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
}
//would rather re-use method of SettingsActivity... but lifecycle...
private String getAutoExportLocationSummary() {
String autoExportLocation = GBApplication.getPrefs().getString(GBPrefs.AUTO_EXPORT_LOCATION, null);
if (autoExportLocation == null) {
return "";
}
Uri uri = Uri.parse(autoExportLocation);
try {
return AndroidUtils.getFilePath(getApplicationContext(), uri);
} catch (IllegalArgumentException e) {
try {
Cursor cursor = getContentResolver().query(
uri,
new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME},
null, null, null, null
);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
LOG.warn("fuck");
}
}
return "";
}
private boolean hasOldActivityDatabase() {
return new DBHelper(this).existsDB("ActivityDatabase");
}

View File

@ -49,6 +49,7 @@ import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import org.slf4j.Logger;
@ -77,6 +78,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
private ScanCallback newLeScanCallback = null;
// Disabled for testing, it seems worse for a few people
private final boolean disableNewBLEScanning = true;
private final Handler handler = new Handler();
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@ -95,7 +99,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
// continue with LE scan, if available
if (isScanning == Scanning.SCANNING_BT) {
checkAndRequestLocationPermission();
if (GBApplication.isRunningLollipopOrLater()) {
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
@ -297,7 +301,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
@Override
protected void onSaveInstanceState(Bundle outState) {
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
}
@ -652,7 +656,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
super.onPause();
stopBTDiscovery();
stopBTLEDiscovery();
if (GBApplication.isRunningLollipopOrLater()) {
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
stopNewBTLEDiscovery();
}
}

View File

@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -97,6 +98,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
return true;
}
});
pref = findPreference("pref_charts");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, ChartsPreferencesActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pref_key_miband");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {

View File

@ -0,0 +1,129 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
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) {
Resources res = getResources();
textView.setText(String.format(res.getQuantityString(R.plurals.widget_alarm_target_hours, userSleepDuration, 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<Alarm> alarms = new ArrayList<>(1);
alarms.add(alarm);
GBApplication.deviceService().onSetAlarms(alarms);
}
}

View File

@ -429,7 +429,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
startActivity(startIntent);
return true;
case R.id.appmanager_app_openinstore:
String url = "https://pebble-appstore.romanport.com/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/0/?query=" + selectedApp.getName();
String url = "https://apps.rebble.io/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1/?native=true&?query=" + Uri.encode(selectedApp.getName());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
startActivity(intent);

View File

@ -26,16 +26,20 @@ 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;
import org.slf4j.Logger;
@ -51,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;
@ -572,7 +572,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 +753,14 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
public static class DefaultChartsData<T extends ChartData<?>> 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 +769,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");
@ -781,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);
@ -791,7 +791,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
}
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
protected static class PreformattedXIndexLabelFormatter extends ValueFormatter {
private ArrayList<String> xLabels;
public PreformattedXIndexLabelFormatter(ArrayList<String> xLabels) {
@ -799,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);

View File

@ -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;
@ -48,6 +47,7 @@ import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -58,7 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
protected final int TOTAL_DAYS = 7;
protected final int TOTAL_DAYS = getRangeDays();
private Locale mLocale;
private int mTargetValue = 0;
@ -87,6 +87,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());
@ -102,6 +106,17 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
// mBalanceView.setText(getBalanceMessage(balance));
}
private String getWeeksChartsLabel(Calendar day){
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
//month, show day date
return String.valueOf(day.get(Calendar.DAY_OF_MONTH));
}
else{
//week, show short day name
return day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale);
}
}
private WeekChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS);
@ -114,7 +129,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
balance += calculateBalance(amounts);
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
labels.add(getWeeksChartsLabel(day));
day.add(Calendar.DATE, 1);
}
@ -130,7 +145,28 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
float average = 0;
if (TOTAL_DAYS > 0) {
average = Math.abs(balance / TOTAL_DAYS);
}
LimitLine average_line = new LimitLine(average);
average_line.setLabel(getString(R.string.average, getAverage(average)));
if (average > (mTargetValue)) {
average_line.setLineColor(Color.GREEN);
average_line.setTextColor(Color.GREEN);
}
else {
average_line.setLineColor(Color.RED);
average_line.setTextColor(Color.RED);
}
if (average > 0) {
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
barChart.getAxisLeft().addLimitLine(average_line);
}
}
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
}
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
@ -315,6 +351,16 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
return amounts;
}
private int getRangeDays(){
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return 30;}
else{
return 7;
}
}
abstract String getAverage(float value);
abstract int getGoal();
abstract int getOffsetHours();
@ -325,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();

View File

@ -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<Integer, Long> stats = new HashMap<Integer, Long>();
// max speed determined from samples
private int maxSpeed = 0;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
public ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> 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);

View File

@ -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();
}

View File

@ -0,0 +1,30 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Canvas;
import com.github.mikephil.charting.animation.ChartAnimator;
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 {
AngledLabelsChartRenderer(BarDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(chart, animator, viewPortHandler);
}
@Override
public void drawValue(Canvas canvas, String valueText, 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(valueText, x, y, mValuePaint);
canvas.restore();
}}

View File

@ -255,12 +255,24 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
this.recreate();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.charts_fetch_activity_data:
fetchActivityData();
return true;
case R.id.prefs_charts_menu:
Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
startActivityForResult(settingsIntent,1);
return true;
default:
break;
}
@ -338,6 +350,24 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return 5;
}
private String getSleepTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weeksleepchart_sleep_a_month);
}
else{
return getString(R.string.weeksleepchart_sleep_a_week);
}
}
public String getStepsTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weekstepschart_steps_a_month);
}
else{
return getString(R.string.weekstepschart_steps_a_week);
}
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
@ -346,9 +376,9 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
case 1:
return getString(R.string.sleepchart_your_sleep);
case 2:
return getString(R.string.weeksleepchart_sleep_a_week);
return getSleepTitle();
case 3:
return getString(R.string.weekstepschart_steps_a_week);
return getStepsTitle();
case 4:
return getString(R.string.stats_title);
case 5:

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, José Rebelo, Szymon Tomasz Stefanek
This file is part of Gadgetbridge.
@ -14,12 +15,17 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
public class GBDeviceEventSleepMonitorResult extends GBDeviceEvent {
// FIXME: this is just the low-level data from Morpheuz, we need something generic
public int smartalarm_from = -1; // time in minutes relative from 0:00 for smart alarm (earliest)
public int smartalarm_to = -1;// time in minutes relative from 0:00 for smart alarm (latest)
public int recording_base_timestamp = -1; // timestamp for the first "point", all folowing are +10 minutes offset each
public int alarm_gone_off = -1; // time in minutes relative from 0:00 when alarm gone off
import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
public class ChartsPreferencesActivity extends AbstractSettingsActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.charts_preferences);
}
}

View File

@ -0,0 +1,102 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
public class SleepAnalysis {
public static final long MIN_SESSION_LENGTH = 5 * 60;
public static final long MAX_WAKE_PHASE_LENGTH = 2 * 60 * 60;
public List<SleepSession> calculateSleepSessions(List<? extends ActivitySample> samples) {
List<SleepSession> result = new ArrayList<>();
ActivitySample previousSample = null;
Date sleepStart = null;
Date sleepEnd = null;
long lightSleepDuration = 0;
long deepSleepDuration = 0;
long durationSinceLastSleep = 0;
for (ActivitySample sample : samples) {
if (isSleep(sample)) {
if (sleepStart == null)
sleepStart = getDateFromSample(sample);
sleepEnd = getDateFromSample(sample);
durationSinceLastSleep = 0;
}
if (previousSample != null) {
long durationSinceLastSample = sample.getTimestamp() - previousSample.getTimestamp();
if (sample.getKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
lightSleepDuration += durationSinceLastSample;
} else if (sample.getKind() == ActivityKind.TYPE_DEEP_SLEEP) {
deepSleepDuration += durationSinceLastSample;
} else {
durationSinceLastSleep += durationSinceLastSample;
if (sleepStart != null && durationSinceLastSleep > MAX_WAKE_PHASE_LENGTH) {
if (lightSleepDuration + deepSleepDuration > MIN_SESSION_LENGTH)
result.add(new SleepSession(sleepStart, sleepEnd, lightSleepDuration, deepSleepDuration));
sleepStart = null;
sleepEnd = null;
lightSleepDuration = 0;
deepSleepDuration = 0;
}
}
}
previousSample = sample;
}
if (lightSleepDuration + deepSleepDuration > MIN_SESSION_LENGTH) {
result.add(new SleepSession(sleepStart, sleepEnd, lightSleepDuration, deepSleepDuration));
}
return result;
}
private boolean isSleep(ActivitySample sample) {
return sample.getKind() == ActivityKind.TYPE_DEEP_SLEEP || sample.getKind() == ActivityKind.TYPE_LIGHT_SLEEP;
}
private Date getDateFromSample(ActivitySample sample) {
return new Date(sample.getTimestamp() * 1000L);
}
public static class SleepSession {
private final Date sleepStart;
private final Date sleepEnd;
private final long lightSleepDuration;
private final long deepSleepDuration;
private SleepSession(Date sleepStart,
Date sleepEnd,
long lightSleepDuration,
long deepSleepDuration) {
this.sleepStart = sleepStart;
this.sleepEnd = sleepEnd;
this.lightSleepDuration = lightSleepDuration;
this.deepSleepDuration = deepSleepDuration;
}
public Date getSleepStart() {
return sleepStart;
}
public Date getSleepEnd() {
return sleepEnd;
}
public long getLightSleepDuration() {
return lightSleepDuration;
}
public long getDeepSleepDuration() {
return deepSleepDuration;
}
}
}

View File

@ -32,29 +32,27 @@ 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.utils.ViewPortHandler;
import com.github.mikephil.charting.formatter.ValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Date;
import java.util.Arrays;
import java.util.Collections;
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.activities.charts.SleepAnalysis.SleepSession;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
@ -83,44 +81,40 @@ public class SleepChartFragment extends AbstractChartFragment {
}
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
SleepAnalysis sleepAnalysis = new SleepAnalysis();
List<SleepSession> sleepSessions = sleepAnalysis.calculateSleepSessions(samples);
PieData data = new PieData();
List<PieEntry> entries = new ArrayList<>();
List<Integer> colors = new ArrayList<>();
// int index = 0;
long totalSeconds = 0;
Date startSleep = null;
Date endSleep = null;
for (ActivityAmount amount : amounts.getAmounts()) {
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
long value = amount.getTotalSeconds();
if(startSleep == null){
startSleep = amount.getStartDate();
} else {
if(startSleep.after(amount.getStartDate()))
startSleep = amount.getStartDate();
}
if(endSleep == null){
endSleep = amount.getEndDate();
} else {
if(endSleep.before(amount.getEndDate()))
endSleep = amount.getEndDate();
}
totalSeconds += value;
// entries.add(new PieEntry(value, index++));
entries.add(new PieEntry(value, amount.getName(getActivity())));
colors.add(getColorFor(amount.getActivityKind()));
// data.addXValue(amount.getName(getActivity()));
}
final long lightSleepDuration = calculateLightSleepDuration(sleepSessions);
final long deepSleepDuration = calculateDeepSleepDuration(sleepSessions);
final long totalSeconds = lightSleepDuration + deepSleepDuration;
final List<PieEntry> entries;
final List<Integer> colors;
if (sleepSessions.isEmpty()) {
entries = Collections.emptyList();
colors = Collections.emptyList();
} else {
entries = Arrays.asList(
new PieEntry(lightSleepDuration, getActivity().getString(R.string.abstract_chart_fragment_kind_light_sleep)),
new PieEntry(deepSleepDuration, getActivity().getString(R.string.abstract_chart_fragment_kind_deep_sleep))
);
colors = Arrays.asList(
getColorFor(ActivityKind.TYPE_LIGHT_SLEEP),
getColorFor(ActivityKind.TYPE_DEEP_SLEEP)
);
}
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) {
public String getFormattedValue(float value) {
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
}
});
@ -132,27 +126,54 @@ public class SleepChartFragment extends AbstractChartFragment {
data.setDataSet(set);
//setupLegend(pieChart);
return new MySleepChartsData(totalSleep, data, startSleep, endSleep);
return new MySleepChartsData(totalSleep, data, sleepSessions);
}
private long calculateLightSleepDuration(List<SleepSession> sleepSessions) {
long result = 0;
for (SleepSession sleepSession : sleepSessions) {
result += sleepSession.getLightSleepDuration();
}
return result;
}
private long calculateDeepSleepDuration(List<SleepSession> sleepSessions) {
long result = 0;
for (SleepSession sleepSession : sleepSessions) {
result += sleepSession.getDeepSleepDuration();
}
return result;
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
mSleepAmountChart.setData(mcd.getPieData().getPieData());
MySleepChartsData pieData = mcd.getPieData();
mSleepAmountChart.setCenterText(pieData.getTotalSleep());
mSleepAmountChart.setData(pieData.getPieData());
mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
mActivityChart.setData(mcd.getChartsData().getData());
if (mcd.getPieData().getStartSleep() != null && mcd.getPieData().getEndSleep() != null) {
mSleepchartInfo.setText(getContext().getString(
R.string.you_slept,
DateTimeUtils.timeToString(mcd.getPieData().getStartSleep()),
DateTimeUtils.timeToString(mcd.getPieData().getEndSleep())));
mSleepchartInfo.setText(buildYouSleptText(pieData));
}
private String buildYouSleptText(MySleepChartsData pieData) {
final StringBuilder result = new StringBuilder();
if (pieData.getSleepSessions().isEmpty()) {
result.append(getContext().getString(R.string.you_did_not_sleep));
} else {
mSleepchartInfo.setText(getContext().getString(R.string.you_did_not_sleep));
for (SleepSession sleepSession : pieData.getSleepSessions()) {
result.append(getContext().getString(
R.string.you_slept,
DateTimeUtils.timeToString(sleepSession.getSleepStart()),
DateTimeUtils.timeToString(sleepSession.getSleepEnd())));
result.append('\n');
}
}
return result.toString();
}
@Override
@ -269,21 +290,19 @@ public class SleepChartFragment extends AbstractChartFragment {
@Override
protected void renderCharts() {
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mActivityChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
mSleepAmountChart.invalidate();
}
private static class MySleepChartsData extends ChartsData {
private String totalSleep;
private final PieData pieData;
private @Nullable Date startSleep;
private @Nullable Date endSleep;
private final List<SleepSession> sleepSessions;
public MySleepChartsData(String totalSleep, PieData pieData, @Nullable Date startSleep, @Nullable Date endSleep) {
public MySleepChartsData(String totalSleep, PieData pieData, List<SleepSession> sleepSessions) {
this.totalSleep = totalSleep;
this.pieData = pieData;
this.startSleep = startSleep;
this.endSleep = endSleep;
this.sleepSessions = sleepSessions;
}
public PieData getPieData() {
@ -294,14 +313,8 @@ public class SleepChartFragment extends AbstractChartFragment {
return totalSleep;
}
@Nullable
public Date getStartSleep() {
return startSleep;
}
@Nullable
public Date getEndSleep() {
return endSleep;
public List<SleepSession> getSleepSessions() {
return sleepSessions;
}
}

View File

@ -16,8 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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 +24,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;
@ -42,7 +41,7 @@ public class TimestampValueFormatter implements IAxisValueFormatter {
}
@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);

View File

@ -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.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.util.ArrayList;
import java.util.List;
@ -40,7 +36,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weeksleepchart_sleep_a_week);
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weeksleepchart_sleep_a_month);
}
else{
return getString(R.string.weeksleepchart_sleep_a_week);
}
}
@Override
@ -110,30 +111,30 @@ 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) {
public String getFormattedValue(float value) {
return formatPieValue((long) value);
}
};
}
@Override
IValueFormatter getBarValueFormatter() {
return new IValueFormatter() {
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);
}
};
}
@Override
IAxisValueFormatter getYAxisFormatter() {
return new IAxisValueFormatter() {
ValueFormatter getYAxisFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
@ -167,4 +168,10 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
private String getHM(long value) {
return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES);
}
@Override
String getAverage(float value) {
return getHM((long)value);
}
}

View File

@ -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;
@ -30,7 +29,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
@Override
public String getTitle() {
return getString(R.string.weekstepschart_steps_a_week);
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weekstepschart_steps_a_month);
}
else{
return getString(R.string.weekstepschart_steps_a_week);
}
}
@Override
@ -77,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;
}
@ -113,4 +117,9 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
} else
return getString(R.string.no_data);
}
@Override
String getAverage(float value) {
return String.format("%.0f", value);
}
}

View File

@ -16,18 +16,21 @@ import org.slf4j.LoggerFactory;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_START;
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;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_OFF;
@ -282,8 +285,10 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
addPreferenceHandlerFor(PREF_SWIPE_UNLOCK);
addPreferenceHandlerFor(PREF_MI2_DATEFORMAT);
addPreferenceHandlerFor("dateformat");
addPreferenceHandlerFor(HuamiConst.PREF_DISPLAY_ITEMS);
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);

View File

@ -33,11 +33,14 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getPrefs;
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
@ -69,6 +72,17 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
GBApplication.deviceService().disconnect();
}
Prefs prefs = getPrefs();
String lastDevice = prefs.getPreferences().getString("last_device_address","");
if (gbDevice.getAddress() == lastDevice){
LOG.debug("#1605 removing last device");
prefs.getPreferences().edit().remove("last_device_address").apply();
}
String macAddress = prefs.getPreferences().getString(MiBandConst.PREF_MIBAND_ADDRESS,"");
if (gbDevice.getAddress() == macAddress){
LOG.debug("#1605 removing devel miband");
prefs.getPreferences().edit().remove(MiBandConst.PREF_MIBAND_ADDRESS).apply();
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device device = DBHelper.findDevice(gbDevice, session);

View File

@ -59,6 +59,10 @@ public class HuamiConst {
public static final String PREF_DISCONNECT_NOTIFICATION_END = "disconnect_notification_end";
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 final String PREF_USE_CUSTOM_FONT = "use_custom_font";
public static int toActivityKind(int rawType) {
switch (rawType) {

View File

@ -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);
@ -251,6 +256,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;

View File

@ -49,6 +49,8 @@ public class HuamiService {
// service uuid fee1
public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_DEVICEEVENT = UUID.fromString("00000010-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_AUDIO = UUID.fromString("00000012-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_AUDIODATA = UUID.fromString("00000013-0000-3512-2118-0009af100700");
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER = UUID.fromString("00000020-0000-3512-2118-0009af100700");
@ -137,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};

View File

@ -47,7 +47,7 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit Bip Watch")) {
if (name != null && (name.equalsIgnoreCase("Amazfit Bip Watch") || name.equalsIgnoreCase("Amazfit Bip Lite"))) {
return DeviceType.AMAZFITBIP;
}
} catch (Exception ex) {
@ -81,8 +81,10 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitbip,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}

View File

@ -84,8 +84,11 @@ public class AmazfitCorCoordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitcor,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_pairingkey};
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -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
};
}

View File

@ -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
};
}

View File

@ -22,7 +22,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.EN
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS;
public class MiBand3Service {
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00};
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x00};
public static final byte[] COMMAND_NIGHT_MODE_OFF = new byte[]{0x1a, 0x00};

View File

@ -90,9 +90,9 @@ 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_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}

View File

@ -0,0 +1,140 @@
/* Copyright (C) 2019 Andreas Shimokawa
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class MijiaLywsd02Coordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && name.equals("LYWSD02")) {
return DeviceType.MIJIA_LYWSD02;
}
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.MIJIA_LYWSD02;
}
@Override
public int getBondingStyle(GBDevice deviceCandidate) {
return BONDING_STYLE_NONE;
}
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 0;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public String getManufacturer() {
return "Xiaomi";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return false;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
// nothing to delete, yet
}
}

View File

@ -22,7 +22,7 @@ import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -34,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
@ -129,7 +130,7 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
// nothing to delete, yet
}
}

View File

@ -158,4 +158,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
public int getBondingStyle(GBDevice device) {
return BONDING_STYLE_NONE;
}
@Override
public boolean supportsUnicodeEmojis() { return true; }
}

View File

@ -0,0 +1,225 @@
/* Copyright (C) 2017-2019 Andreas Shimokawa
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 <http://www.gnu.org/licenses/>. */
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 android.os.Build;
import androidx.annotation.RequiresApi;
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;
@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);
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<WeatherInfo.DayForecast> 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<WeatherLocation> 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);
}
}
}

View File

@ -27,13 +27,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.RemoteException;
@ -44,6 +42,12 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.palette.graphics.Palette;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -53,11 +57,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.palette.graphics.Palette;
import de.greenrobot.dao.query.Query;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -65,11 +64,11 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilter;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterDao;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntry;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntryDao;
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@ -242,9 +241,11 @@ public class NotificationListener extends NotificationListenerService {
public void onNotificationPosted(StatusBarNotification sbn) {
Prefs prefs = GBApplication.getPrefs();
if ("call".equals(sbn.getNotification().category) && prefs.getBoolean("notification_support_voip_calls", false)) {
handleCallNotification(sbn);
return;
if (GBApplication.isRunningLollipopOrLater()) {
if ("call".equals(sbn.getNotification().category) && prefs.getBoolean("notification_support_voip_calls", false)) {
handleCallNotification(sbn);
return;
}
}
if (shouldIgnore(sbn)) {
LOG.info("Ignore notification");
@ -531,6 +532,9 @@ public class NotificationListener extends NotificationListenerService {
Bundle extras = NotificationCompat.getExtras(notification);
//dumpExtras(extras);
if (extras == null) {
return;
}
CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE);
if (title != null) {
@ -582,13 +586,13 @@ public class NotificationListener extends NotificationListenerService {
stateSpec.repeat = 1;
stateSpec.shuffle = 1;
switch (s.getState()) {
case PlaybackState.STATE_PLAYING:
case PlaybackStateCompat.STATE_PLAYING:
stateSpec.state = MusicStateSpec.STATE_PLAYING;
break;
case PlaybackState.STATE_STOPPED:
case PlaybackStateCompat.STATE_STOPPED:
stateSpec.state = MusicStateSpec.STATE_STOPPED;
break;
case PlaybackState.STATE_PAUSED:
case PlaybackStateCompat.STATE_PAUSED:
stateSpec.state = MusicStateSpec.STATE_PAUSED;
break;
default:
@ -624,12 +628,15 @@ public class NotificationListener extends NotificationListenerService {
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
LOG.info("Notification removed: " + sbn.getPackageName() + ": " + sbn.getNotification().category);
if(Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
activeCallPostTime = 0;
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_END;
GBApplication.deviceService().onSetCallState(callSpec);
LOG.info("Notification removed: " + sbn.getPackageName());
if (GBApplication.isRunningLollipopOrLater()) {
LOG.info("Notification removed: " + sbn.getPackageName() + ", category: " + sbn.getNotification().category);
if (Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
activeCallPostTime = 0;
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_END;
GBApplication.deviceService().onSetCallState(callSpec);
}
}
// FIXME: DISABLED for now
/*

View File

@ -116,9 +116,9 @@ public class ActivityKind {
case TYPE_NOT_MEASURED:
return R.drawable.ic_activity_not_measured;
case TYPE_LIGHT_SLEEP:
return R.drawable.ic_activity_light_sleep;
return R.drawable.ic_activity_sleep;
case TYPE_DEEP_SLEEP:
return R.drawable.ic_activity_deep_sleep;
return R.drawable.ic_activity_sleep;
case TYPE_RUNNING:
return R.drawable.ic_activity_running;
case TYPE_WALKING:
@ -128,7 +128,9 @@ public class ActivityKind {
case TYPE_TREADMILL:
return R.drawable.ic_activity_walking;
case TYPE_EXERCISE: // fall through
return R.drawable.ic_activity_exercise;
case TYPE_SWIMMING: // fall through
return R.drawable.ic_activity_swimming;
case TYPE_NOT_WORN: // fall through
case TYPE_ACTIVITY: // fall through
case TYPE_UNKNOWN: // fall through

View File

@ -0,0 +1,149 @@
/* 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 <http://www.gnu.org/licenses/>. */
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 {
private static final Logger LOG = LoggerFactory.getLogger(DailyTotals.class);
public int[] getDailyTotalsForAllDevices(Calendar day) {
Context context = GBApplication.getContext();
//get today's steps for all devices in GB
int all_steps = 0;
int all_sleep = 0;
if (context instanceof GBApplication) {
GBApplication gbApp = (GBApplication) context;
List<? extends GBDevice> devices = gbApp.getDeviceManager().getDevices();
for (GBDevice device : devices) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
if (!coordinator.supportsActivityDataFetching()) {
continue;
}
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 int[]{all_steps, all_sleep};
}
public int[] getDailyTotalsForDevice(GBDevice device, Calendar day) {
try (DBHandler handler = GBApplication.acquireDB()) {
ActivityAnalysis analysis = new ActivityAnalysis();
ActivityAmounts amountsSteps;
ActivityAmounts amountsSleep;
amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device));
amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device));
int[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep);
int Steps = getTotalsStepsForActivityAmounts(amountsSteps);
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 int[]{0, 0, 0};
}
}
private int[] 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 int[]{totalMinutesDeepSleep, totalMinutesLightSleep};
}
private int getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) {
int totalSteps = 0;
for (ActivityAmount amount : activityAmounts.getAmounts()) {
totalSteps += amount.getTotalSteps();
}
return totalSteps;
}
private List<? extends ActivitySample> 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<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return getAllSamples(db, device, tsFrom, tsTo);
}
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
return coordinator.getSampleProvider(device, db.getDaoSession());
}
protected List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
return provider.getAllActivitySamples(tsFrom, tsTo);
}
}

View File

@ -57,6 +57,7 @@ public enum DeviceType {
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
MISCALE2(131, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_miscale2),
BFH16(140, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_bfh16),
MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;

View File

@ -58,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -151,8 +150,6 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
handleGBDeviceEvent((GBDeviceEventVersionInfo) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventAppInfo) {
handleGBDeviceEvent((GBDeviceEventAppInfo) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventSleepMonitorResult) {
handleGBDeviceEvent((GBDeviceEventSleepMonitorResult) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventScreenshot) {
handleGBDeviceEvent((GBDeviceEventScreenshot) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventNotificationControl) {
@ -258,18 +255,6 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
LocalBroadcastManager.getInstance(context).sendBroadcast(appInfoIntent);
}
private void handleGBDeviceEvent(GBDeviceEventSleepMonitorResult sleepMonitorResult) {
Context context = getContext();
LOG.info("Got event for SLEEP_MONIOR_RES");
Intent sleepMonitorIntent = new Intent(ChartsHost.REFRESH);
sleepMonitorIntent.putExtra("smartalarm_from", sleepMonitorResult.smartalarm_from);
sleepMonitorIntent.putExtra("smartalarm_to", sleepMonitorResult.smartalarm_to);
sleepMonitorIntent.putExtra("recording_base_timestamp", sleepMonitorResult.recording_base_timestamp);
sleepMonitorIntent.putExtra("alarm_gone_off", sleepMonitorResult.alarm_gone_off);
LocalBroadcastManager.getInstance(context).sendBroadcast(sleepMonitorIntent);
}
private void handleGBDeviceEvent(GBDeviceEventScreenshot screenshot) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-hhmmss", Locale.US);
String filename = "screenshot_" + dateFormat.format(new Date()) + ".bmp";
@ -409,4 +394,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
LocalBroadcastManager.getInstance(context).sendBroadcast(messageIntent);
}
public String customStringFilter(String inputString) {
return inputString;
}
}

View File

@ -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,19 +47,18 @@ 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;
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;
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;
@ -78,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;
@ -96,12 +100,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;
@ -193,8 +197,8 @@ 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();
private final String[] mMusicActions = {
"com.android.music.metachanged",
@ -365,8 +369,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if (text == null || text.length() == 0)
return text;
if (!mCoordinator.supportsUnicodeEmojis())
text = mDeviceSupport.customStringFilter(text);
if (!mCoordinator.supportsUnicodeEmojis()) {
return EmojiConverter.convertUnicodeEmojiToAscii(text, getApplicationContext());
}
return text;
}
@ -733,10 +740,17 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mCMWeatherReceiver = new CMWeatherReceiver();
registerReceiver(mCMWeatherReceiver, 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 {
mOmniJawsObserver = new OmniJawsObserver(new Handler());
getContentResolver().registerContentObserver(mOmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
getContentResolver().registerContentObserver(OmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
} catch (PackageManager.NameNotFoundException e) {
//Nothing wrong, it just means we're not running on omnirom.
}
@ -784,6 +798,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);
}

View File

@ -130,4 +130,9 @@ public interface DeviceSupport extends EventHandler {
* Returns the Android context to use, e.g. to look up resources.
*/
Context getContext();
/**
* converts String in a device specific way, e.g. re-map characters for a custom font
*/
String customStringFilter(String inputString);
}

View File

@ -30,19 +30,20 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900.CasioGB6900DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor.AmazfitCorSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand3Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
@ -58,13 +59,13 @@ public class DeviceSupportFactory {
private final BluetoothAdapter mBtAdapter;
private final Context mContext;
public DeviceSupportFactory(Context context) {
DeviceSupportFactory(Context context) {
mContext = context;
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
}
public synchronized DeviceSupport createDeviceSupport(GBDevice device) throws GBException {
DeviceSupport deviceSupport = null;
DeviceSupport deviceSupport;
String deviceAddress = device.getAddress();
int indexFirstColon = deviceAddress.indexOf(":");
if (indexFirstColon > 0) {
@ -196,6 +197,8 @@ public class DeviceSupportFactory {
case BFH16:
deviceSupport = new ServiceDeviceSupport(new BFH16DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case MIJIA_LYWSD02:
deviceSupport = new ServiceDeviceSupport(new MijiaLywsd02Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);

View File

@ -113,6 +113,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
return delegate.getContext();
}
@Override
public String customStringFilter(String inputString) {
return delegate.customStringFilter(inputString);
}
@Override
public boolean useAutoConnect() {
return delegate.useAutoConnect();

View File

@ -15,7 +15,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,43 +33,35 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ActivityDetailsParser {
private static final Logger LOG = LoggerFactory.getLogger(ActivityDetailsParser.class);
public class HuamiActivityDetailsParser {
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
private static final byte TYPE_GPS = 0;
private static final byte TYPE_HR = 1;
private static final byte TYPE_UNKNOWN2 = 2;
private static final byte TYPE_PAUSE = 3;
private static final byte TYPE_PAUSE = 2;
private static final byte TYPE_RESUME = 3;
private static final byte TYPE_SPEED4 = 4;
private static final byte TYPE_SPEED5 = 5;
private static final byte TYPE_GPS_SPEED6 = 6;
private static final byte TYPE_SPEED6 = 6;
private static final byte TYPE_SWIMMING = 8;
public static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
private final BaseActivitySummary summary;
private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
private final ActivityTrack activityTrack;
// private final int version;
private final Date baseDate;
private long baseLongitude;
private long baseLatitude;
private int baseAltitude;
private ActivityPoint lastActivityPoint;
public boolean getSkipCounterByte() {
return skipCounterByte;
}
public void setSkipCounterByte(boolean skipCounterByte) {
this.skipCounterByte = skipCounterByte;
}
private boolean skipCounterByte;
public ActivityDetailsParser(BaseActivitySummary summary) {
this.summary = summary;
// this.version = version;
// this.baseDate = baseDate;
//
public HuamiActivityDetailsParser(BaseActivitySummary summary) {
this.baseLongitude = summary.getBaseLongitude();
this.baseLatitude = summary.getBaseLatitude();
this.baseAltitude = summary.getBaseAltitude();
@ -109,21 +101,24 @@ public class ActivityDetailsParser {
case TYPE_HR:
i += consumeHeartRate(bytes, i, totalTimeOffset);
break;
case TYPE_UNKNOWN2:
i += consumeUnknown2(bytes, i);
break;
case TYPE_PAUSE:
i += consumePause(bytes, i);
break;
case TYPE_RESUME:
i += consumeResume(bytes, i);
break;
case TYPE_SPEED4:
i += consumeSpeed4(bytes, i);
break;
case TYPE_SPEED5:
i += consumeSpeed5(bytes, i);
break;
case TYPE_GPS_SPEED6:
case TYPE_SPEED6:
i += consumeSpeed6(bytes, i);
break;
case TYPE_SWIMMING:
i += consumeSwimming(bytes, i);
break;
default:
LOG.warn("unknown packet type" + type);
i+=6;
@ -213,7 +208,6 @@ public class ActivityDetailsParser {
if (v2 == 0 && v3 == 0 && v4 == 0 && v5 == 0 && v6 == 0) {
// new version
// LOG.info("detected heart rate in 'new' version, where version is: " + summary.getVersion());
LOG.info("detected heart rate in 'new' version format");
ActivityPoint ap = getActivityPointFor(timeOffsetSeconds);
ap.setHeartRate(v1);
@ -270,23 +264,33 @@ public class ActivityDetailsParser {
}
}
private int consumeUnknown2(byte[] bytes, int offset) {
return 6; // just guessing...
private int consumePause(byte[] bytes, int offset) {
LOG.debug("got pause packet: " + GB.hexdump(bytes, offset, 6));
return 6;
}
private int consumePause(byte[] bytes, int i) {
return 6; // just guessing...
private int consumeResume(byte[] bytes, int offset) {
LOG.debug("got resume package: " + GB.hexdump(bytes, offset, 6));
return 6;
}
private int consumeSpeed4(byte[] bytes, int offset) {
LOG.debug("got packet type 4 (speed): " + GB.hexdump(bytes, offset, 6));
return 6;
}
private int consumeSpeed5(byte[] bytes, int offset) {
LOG.debug("got packet type 5 (speed): " + GB.hexdump(bytes, offset, 6));
return 6;
}
private int consumeSpeed6(byte[] bytes, int offset) {
LOG.debug("got packet type 6 (speed): " + GB.hexdump(bytes, offset, 6));
return 6;
}
private int consumeSwimming(byte[] bytes, int offset) {
LOG.debug("got packet type 8 (swimming?): " + GB.hexdump(bytes, offset, 6));
return 6;
}
}

View File

@ -14,20 +14,20 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public enum BipActivityType {
public enum HuamiSportsActivityType {
Outdoor(1),
Treadmill(2),
Walking(3),
Cycling(4),
Exercise(5);
Exercise(5),
Swimming(6);
private final int code;
BipActivityType(final int code) {
HuamiSportsActivityType(final int code) {
this.code = code;
}
@ -43,20 +43,22 @@ public enum BipActivityType {
return ActivityKind.TYPE_WALKING;
case Exercise:
return ActivityKind.TYPE_EXERCISE;
case Swimming:
return ActivityKind.TYPE_SWIMMING;
}
throw new RuntimeException("Not mapped activity kind for: " + this);
}
public static BipActivityType fromCode(int bipCode) {
for (BipActivityType type : values()) {
if (type.code == bipCode) {
public static HuamiSportsActivityType fromCode(int huamiCode) {
for (HuamiSportsActivityType type : values()) {
if (type.code == huamiCode) {
return type;
}
}
throw new RuntimeException("No matching BipActivityType for code: " + bipCode);
throw new RuntimeException("No matching HuamiSportsActivityType for code: " + huamiCode);
}
public static BipActivityType fromActivityKind(int activityKind) {
public static HuamiSportsActivityType fromActivityKind(int activityKind) {
switch (activityKind) {
case ActivityKind.TYPE_RUNNING:
return Outdoor;
@ -68,6 +70,8 @@ public enum BipActivityType {
return Walking;
case ActivityKind.TYPE_EXERCISE:
return Exercise;
case ActivityKind.TYPE_SWIMMING:
return Swimming;
}
throw new RuntimeException("No matching activity activityKind: " + activityKind);
}

View File

@ -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;
@ -123,23 +127,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
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;
@ -301,6 +294,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIO), enable);
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIODATA), enable);
return this;
}
@ -325,23 +320,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) {
@ -437,7 +415,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
int userid = alias.hashCode(); // hash from alias like mi1
// FIXME: Do encoding like in PebbleProtocol, this is ugly
byte bytes[] = new byte[]{
byte[] bytes = new byte[]{
HuamiService.COMMAND_SET_USERINFO,
0,
0,
@ -561,58 +539,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);
@ -682,7 +628,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
sendCalendarEvents(builder);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to set time on MI device", ex);
LOG.error("Unable to set time on Huami device", ex);
}
}
@ -701,17 +647,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");
}
}
@ -760,9 +706,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
private void sendMusicStateToDevice() {
if (characteristicChunked == null) {
return;
}
@ -878,7 +823,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
builder.write(characteristicHRControlPoint, startHeartMeasurementManual);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to read heart rate with MI2", ex);
LOG.error("Unable to read heart rate from Huami device", ex);
}
}
@ -939,7 +884,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
try {
new FetchActivityOperation(this).perform();
} catch (IOException ex) {
LOG.error("Unable to fetch MI activity data", ex);
LOG.error("Unable to fetch activity data", ex);
}
}
@ -969,7 +914,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) {
byte result[] = new byte[12];
byte[] result = new byte[12];
result[0] = (byte) (minConnectionInterval & 0xff);
result[1] = (byte) (0xff & minConnectionInterval >> 8);
result[2] = (byte) (maxConnectionInterval & 0xff);
@ -1557,9 +1502,15 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
case MiBandConst.PREF_SWIPE_UNLOCK:
setBandScreenUnlock(builder);
break;
case "dateformat":
case HuamiConst.PREF_DATEFORMAT:
setDateFormat(builder);
break;
case HuamiConst.PREF_LANGUAGE:
setLanguage(builder);
break;
case HuamiConst.PREF_EXPOSE_HR_THIRDPARTY:
setExposeHRThridParty(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
@ -1579,7 +1530,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, 1, 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) {
@ -1904,6 +2038,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;
@ -1932,6 +2080,42 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
}
@Override
public String customStringFilter(String inputString) {
if (HuamiCoordinator.getUseCustomFont(gbDevice.getAddress())) {
return convertEmojiToCustomFont(inputString);
}
return inputString;
}
private String convertEmojiToCustomFont(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((char) (codePoint - 83712));
i = i2;
}
} catch (StringIndexOutOfBoundsException e) {
LOG.warn("error while converting emoji to custom font", e);
sb.append(charAt);
}
} else {
sb.append(charAt);
}
i++;
}
return sb.toString();
}
public void phase2Initialize(TransactionBuilder builder) {
LOG.info("phase2Initialize...");
requestBatteryInfo(builder);
@ -1954,6 +2138,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
setInactivityWarnings(builder);
setHeartrateSleepSupport(builder);
setDisconnectNotification(builder);
setExposeHRThridParty(builder);
setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval());
}

View File

@ -107,6 +107,11 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
crcToVersion.put(60002, "1.1.5.04");
crcToVersion.put(5229, "1.1.5.12");
crcToVersion.put(32576, "1.1.5.16");
crcToVersion.put(28893, "1.1.5.24");
crcToVersion.put(61710, "1.1.5.56");
// Latin Firmware
crcToVersion.put(52828, "1.1.5.36 (Latin)");
// resources
crcToVersion.put(12586, "0.0.8.74");
@ -130,7 +135,9 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
crcToVersion.put(23073, "0.1.1.45");
crcToVersion.put(59245, "1.0.2.00");
crcToVersion.put(20591, "1.1.2.05");
crcToVersion.put(5341, "1.1.5.02-16");
crcToVersion.put(5341, "1.1.5.02-24");
crcToVersion.put(22662, "1.1.5.36");
crcToVersion.put(24045, "1.1.5.56");
// gps
crcToVersion.put(61520, "9367,8f79a91,0,0,");

View File

@ -28,43 +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.Locale;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.UUID;
import cyanogenmod.weather.util.WeatherUtils;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
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.impl.GBDevice;
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.actions.ConditionalWriteAction;
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.amazfitbip.operations.AmazfitBipFetchLogsOperation;
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 {
@ -252,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 {
@ -442,9 +248,8 @@ public class AmazfitBipSupport extends HuamiSupport {
} else if (dataTypes == RecordedDataTypes.TYPE_GPS_TRACKS) {
new FetchSportsSummaryOperation(this).perform();
} else if (dataTypes == RecordedDataTypes.TYPE_DEBUGLOGS) {
new AmazfitBipFetchLogsOperation(this).perform();
}
else {
new HuamiFetchDebugLogsOperation(this).perform();
} else {
LOG.warn("fetching multiple data types at once is not supported yet");
}
} catch (IOException ex) {

View File

@ -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});
}
}

View File

@ -86,9 +86,13 @@ public class MiBand3Support extends AmazfitBipSupport {
command[1] |= 0x80;
command[10] = pos++;
}
if (pages.contains("nfc")) {
command[2] |= 0x01;
command[11] = pos++;
}
}
for (int i = 4; i <= 10; i++) {
for (int i = 4; i <= 11; i++) {
if (command[i] == 0) {
command[i] = pos++;
}

View File

@ -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");
@ -65,7 +69,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;
}
}

View File

@ -85,6 +85,7 @@ public class FetchActivityOperation extends AbstractFetchOperation {
}
super.handleActivityFetchFinish(success);
GB.signalActivityDataFinish();
}
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {

View File

@ -40,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.ActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -86,7 +86,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
if (success) {
ActivityDetailsParser parser = new ActivityDetailsParser(summary);
HuamiActivityDetailsParser parser = new HuamiActivityDetailsParser(summary);
parser.setSkipCounterByte(false); // is already stripped
try {
ActivityTrack track = parser.parse(buffer.toByteArray());

View File

@ -43,8 +43,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.BipActivityType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -141,7 +141,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
return;
}
if ((byte) (lastPacketCounter + 1) == value[0] ) {
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
@ -154,6 +154,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
/**
* Buffers the given activity summary data. If the total size is reached,
* it is converted to an object and saved in the database.
*
* @param value
*/
@Override
@ -166,14 +167,14 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
ByteBuffer buffer = ByteBuffer.wrap(stream.toByteArray()).order(ByteOrder.LITTLE_ENDIAN);
// summary.setVersion(BLETypeConversions.toUnsigned(buffer.getShort()));
short version = buffer.getShort(); // version
LOG.debug("Got verison " + version);
LOG.debug("Got sport summary version " + version + "total bytes=" + buffer.capacity());
int activityKind = ActivityKind.TYPE_UNKNOWN;
try {
int rawKind = BLETypeConversions.toUnsigned(buffer.getShort());
BipActivityType activityType = BipActivityType.fromCode(rawKind);
HuamiSportsActivityType activityType = HuamiSportsActivityType.fromCode(rawKind);
activityKind = activityType.toActivityKind();
} catch (Exception ex) {
LOG.error("Error mapping acivity kind: " + ex.getMessage(), ex);
LOG.error("Error mapping activity kind: " + ex.getMessage(), ex);
}
summary.setActivityKind(activityKind);
@ -197,44 +198,117 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
summary.setBaseLongitude(baseLongitude);
summary.setBaseLatitude(baseLatitude);
summary.setBaseAltitude(baseAltitude);
// unused data (for now)
float distanceMeters = buffer.getFloat();
float ascentMeters = buffer.getFloat();
float descentMeters = buffer.getFloat();
float maxAltitude = buffer.getFloat();
float minAltitude = buffer.getFloat();
int maxLatitude = buffer.getInt(); // format?
int minLatitude = buffer.getInt(); // format?
int maxLongitude = buffer.getInt(); // format?
int minLongitude = buffer.getInt(); // format?
int steps = buffer.getInt();
int activeSeconds = buffer.getInt();
float caloriesBurnt = buffer.getFloat();
float maxSpeed = buffer.getFloat();
float minPace = buffer.getFloat(); // format?
float maxPace = buffer.getFloat(); // format?
float totalStride = buffer.getFloat();
buffer.getInt(); // unknown
if (activityKind == ActivityKind.TYPE_SWIMMING) {
// 28 bytes
float averageStrokeDistance = buffer.getFloat();
float averageStrokesPerSecond = buffer.getFloat();
float averageLapPace = buffer.getFloat();
short strokes = buffer.getShort();
short swolfIndex = buffer.getShort();
byte swimStyle = buffer.get();
byte laps = buffer.get();
buffer.getInt(); // unknown
buffer.getInt(); // unknown
buffer.getShort(); // unknown
LOG.debug("unused swim data:" +
"\naverageStrokeDistance=" + averageStrokeDistance +
"\naverageStrokesPerSecond=" + averageStrokesPerSecond +
"\naverageLapPace" + averageLapPace +
"\nstrokes=" + strokes +
"\nswolfIndex=" + swolfIndex +
"\nswimStyle=" + swimStyle + // 1 = breast, 2 = freestyle
"\nlaps=" + laps +
""
);
} else {
// 28 bytes
buffer.getInt(); // unknown
buffer.getInt(); // unknown
int ascentSeconds = buffer.getInt() / 1000; //ms?
buffer.getInt(); // unknown;
int descentSeconds = buffer.getInt() / 1000; //ms?
buffer.getInt(); // unknown;
int flatSeconds = buffer.getInt() / 1000; // ms?
LOG.debug("unused non-swim data:" +
"\nascentSeconds=" + ascentSeconds +
"\ndescentSeconds=" + descentSeconds +
"\nflatSeconds=" + flatSeconds +
""
);
}
short averageHR = buffer.getShort();
short averageKMPaceSeconds = buffer.getShort();
short averageStride = buffer.getShort();
LOG.debug("unused common:" +
"\ndistanceMeters=" + distanceMeters +
"\nascentMeters=" + ascentMeters +
"\ndescentMeters=" + descentMeters +
"\nmaxAltitude=" + maxAltitude +
"\nminAltitude=" + minAltitude +
//"\nmaxLatitude=" + maxLatitude + // not useful
//"\nminLatitude=" + minLatitude + // not useful
//"\nmaxLongitude=" + maxLongitude + // not useful
//"\nminLongitude=" + minLongitude + // not useful
"\nsteps=" + steps +
"\nactiveSeconds=" + activeSeconds +
"\ncaloriesBurnt=" + caloriesBurnt +
"\nmaxSpeed=" + maxSpeed +
"\nminPace=" + minPace +
"\nmaxPace=" + maxPace +
"\ntotalStride=" + totalStride +
"\naverageHR=" + averageHR +
"\naverageKMPaceSeconds=" + averageKMPaceSeconds +
"\naverageStride=" + averageStride +
""
);
// summary.setBaseCoordinate(new GPSCoordinate(baseLatitude, baseLongitude, baseAltitude));
// summary.setDistanceMeters(Float.intBitsToFloat(buffer.getInt()));
// summary.setAscentMeters(Float.intBitsToFloat(buffer.getInt()));
// summary.setDescentMeters(Float.intBitsToFloat(buffer.getInt()));
//
// summary.setMinAltitude(Float.intBitsToFloat(buffer.getInt()));
// summary.setMaxAltitude(Float.intBitsToFloat(buffer.getInt()));
// summary.setMinLatitude(buffer.getInt());
// summary.setMaxLatitude(buffer.getInt());
// summary.setMinLongitude(buffer.getInt());
// summary.setMaxLongitude(buffer.getInt());
//
// summary.setSteps(BLETypeConversions.toUnsigned(buffer.getInt()));
// summary.setActiveTimeSeconds(BLETypeConversions.toUnsigned(buffer.getInt()));
//
// summary.setCaloriesBurnt(Float.intBitsToFloat(buffer.get()));
// summary.setMaxSpeed(Float.intBitsToFloat(buffer.get()));
// summary.setMinPace(Float.intBitsToFloat(buffer.get()));
// summary.setMaxPace(Float.intBitsToFloat(buffer.get()));
// summary.setTotalStride(Float.intBitsToFloat(buffer.get()));
buffer.getInt(); //
buffer.getInt(); //
buffer.getInt(); //
// summary.setTimeAscent(BLETypeConversions.toUnsigned(buffer.getInt()));
// buffer.getInt(); //
// summary.setTimeDescent(BLETypeConversions.toUnsigned(buffer.getInt()));
// buffer.getInt(); //
// summary.setTimeFlat(BLETypeConversions.toUnsigned(buffer.getInt()));
//
// summary.setAverageHR(BLETypeConversions.toUnsigned(buffer.getShort()));
//
// summary.setAveragePace(BLETypeConversions.toUnsigned(buffer.getShort()));
// summary.setAverageStride(BLETypeConversions.toUnsigned(buffer.getShort()));
buffer.getShort(); //
// summary.setDistanceMeters(distanceMeters);
// summary.setAscentMeters(ascentMeters);
// summary.setDescentMeters(descentMeters);
// summary.setMinAltitude(maxAltitude);
// summary.setMaxAltitude(maxAltitude);
// summary.setMinLatitude(minLatitude);
// summary.setMaxLatitude(maxLatitude);
// summary.setMinLongitude(minLatitude);
// summary.setMaxLongitude(maxLatitude);
// summary.setSteps(steps);
// summary.setActiveTimeSeconds(secondsActive);
// summary.setCaloriesBurnt(caloriesBurnt);
// summary.setMaxSpeed(maxSpeed);
// summary.setMinPace(minPace);
// summary.setMaxPace(maxPace);
// summary.setTotalStride(totalStride);
// summary.setTimeAscent(BLETypeConversions.toUnsigned(ascentSeconds);
// summary.setTimeDescent(BLETypeConversions.toUnsigned(descentSeconds);
// summary.setTimeFlat(BLETypeConversions.toUnsigned(flatSeconds);
// summary.setAverageHR(BLETypeConversions.toUnsigned(averageHR);
// summary.setAveragePace(BLETypeConversions.toUnsigned(averagePace);
// summary.setAverageStride(BLETypeConversions.toUnsigned(averageStride);
return summary;
}

View File

@ -15,7 +15,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations;
import android.widget.Toast;
@ -36,18 +36,17 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipS
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipFetchLogsOperation.class);
public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(HuamiFetchDebugLogsOperation.class);
private FileOutputStream logOutputStream;
public AmazfitBipFetchLogsOperation(AmazfitBipSupport support) {
public HuamiFetchDebugLogsOperation(AmazfitBipSupport support) {
super(support);
setName("fetch logs");
setName("fetch debug logs");
}
@Override
@ -60,7 +59,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation {
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
String filename = "amazfitbip_" + dateFormat.format(new Date()) + ".log";
String filename = "huamidebug_" + dateFormat.format(new Date()) + ".log";
File outputFile = new File(dir, filename );
try {

View File

@ -49,9 +49,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
//import java.util.concurrent.Executors;
//import java.util.concurrent.ScheduledExecutorService;
//import java.util.concurrent.ScheduledFuture;
/**
* An operation that fetches activity data. For every fetch, a new operation must
@ -71,7 +68,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
private final boolean hasPacketCounter;
private class ActivityStruct {
private int maxDataPacketLength = 20;
private int maxDataPacketLength;
private int lastNotifiedProgress;
private final byte[] activityDataHolder;
private final int activityDataHolderSize;
@ -197,6 +194,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
activityStruct = null;
operationFinished();
unsetBusy();
GB.signalActivityDataFinish();
}
/**

View File

@ -0,0 +1,293 @@
/* Copyright (C) 2019 Andreas Shimokawa
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Objects;
import java.util.SimpleTimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
public class MijiaLywsd02Support extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(MijiaLywsd02Support.class);
private final DeviceInfoProfile<MijiaLywsd02Support> deviceInfoProfile;
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
private final IntentListener mListener = new IntentListener() {
@Override
public void notify(Intent intent) {
String s = intent.getAction();
if (Objects.equals(s, DeviceInfoProfile.ACTION_DEVICE_INFO)) {
handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
}
}
};
public MijiaLywsd02Support() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(UUID.fromString("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"));
deviceInfoProfile = new DeviceInfoProfile<>(this);
deviceInfoProfile.addListener(mListener);
addSupportedProfile(deviceInfoProfile);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
requestDeviceInfo(builder);
setTime(builder);
setInitialized(builder);
return builder;
}
private void setTime(TransactionBuilder builder) {
BluetoothGattCharacteristic timeCharacteristc = getCharacteristic(UUID.fromString("ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6"));
long ts = System.currentTimeMillis();
byte offsetHours = (byte) (SimpleTimeZone.getDefault().getOffset(ts) / (1000 * 60 * 60));
ts /= 1000;
builder.write(timeCharacteristc, new byte[]{
(byte) (ts & 0xff),
(byte) ((ts >> 8) & 0xff),
(byte) ((ts >> 16) & 0xff),
(byte) ((ts >> 24) & 0xff),
offsetHours});
}
private void requestDeviceInfo(TransactionBuilder builder) {
LOG.debug("Requesting Device Info!");
deviceInfoProfile.requestDeviceInfo(builder);
}
private void setInitialized(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
}
@Override
public boolean useAutoConnect() {
return false;
}
private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
LOG.warn("Device info: " + info);
versionCmd.hwVersion = info.getHardwareRevision();
versionCmd.fwVersion = info.getFirmwareRevision();
handleGBDeviceEvent(versionCmd);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
// better only on connect for now
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
}
@Override
public void onSetCallState(CallSpec callSpec) {
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public void onAppInfoReq() {
}
@Override
public void onAppStart(UUID uuid, boolean start) {
}
@Override
public void onAppDelete(UUID uuid) {
}
@Override
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
}
@Override
public void onAppReorder(UUID[] uuids) {
}
@Override
public void onFetchRecordedData(int dataTypes) {
}
@Override
public void onReset(int flags) {
}
@Override
public void onHeartRateTest() {
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
}
@Override
public void onFindDevice(boolean start) {
}
@Override
public void onSetConstantVibration(int intensity) {
}
@Override
public void onScreenshotReq() {
}
@Override
public void onEnableHeartRateSleepSupport(boolean enable) {
}
@Override
public void onSetHeartRateMeasurementInterval(int seconds) {
}
@Override
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
}
@Override
public void onDeleteCalendarEvent(byte type, long id) {
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
UUID characteristicUUID = characteristic.getUuid();
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
return false;
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
if (super.onCharacteristicRead(gatt, characteristic, status)) {
return true;
}
UUID characteristicUUID = characteristic.getUuid();
LOG.info("Unhandled characteristic read: " + characteristicUUID);
return false;
}
@Override
public void onSendConfiguration(String config) {
}
@Override
public void onReadConfiguration(String config) {
}
@Override
public void onTestNewFunction() {
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
}

View File

@ -628,7 +628,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport {
GB.updateTransferNotification(null,"", false, 100, getContext());
if (getDevice().isBusy()) {
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
GB.signalActivityDataFinish();
}
}
} catch (Exception ex) {

View File

@ -105,7 +105,7 @@ class AppMessageHandlerM7S extends AppMessageHandler {
return HAIL;
} else if (conditionCode >= 907 && conditionCode < 957) {
return WIND;
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
} else if (conditionCode == 905) {
return EXTREME_WIND;
} else if (conditionCode == 900) {
return TORNADO;

View File

@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMisfitSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
class AppMessageHandlerMisfit extends AppMessageHandler {
@ -71,6 +72,7 @@ class AppMessageHandlerMisfit extends AppMessageHandler {
LOG.info("incoming data start");
break;
case KEY_INCOMING_DATA_END:
GB.signalActivityDataFinish();
LOG.info("incoming data end");
break;
case KEY_INCOMING_DATA:

View File

@ -35,7 +35,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMorpheuzSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -70,7 +69,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
private static final Logger LOG = LoggerFactory.getLogger(AppMessageHandlerMorpheuz.class);
public AppMessageHandlerMorpheuz(UUID uuid, PebbleProtocol pebbleProtocol) {
AppMessageHandlerMorpheuz(UUID uuid, PebbleProtocol pebbleProtocol) {
super(uuid, pebbleProtocol);
try {
@ -108,16 +107,11 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
@Override
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
int ctrl_message = 0;
GBDeviceEventSleepMonitorResult sleepMonitorResult = null;
for (Pair<Integer, Object> pair : pairs) {
if (Objects.equals(pair.first, keyTransmit)) {
sleepMonitorResult = new GBDeviceEventSleepMonitorResult();
sleepMonitorResult.smartalarm_from = smartalarm_from;
sleepMonitorResult.smartalarm_to = smartalarm_to;
sleepMonitorResult.alarm_gone_off = alarm_gone_off;
sleepMonitorResult.recording_base_timestamp = recording_base_timestamp;
ctrl_message |= CTRL_TRANSMIT_DONE;
GB.signalActivityDataFinish();
} else if (pair.first.equals(keyGoneoff)) {
alarm_gone_off = (int) pair.second;
LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60);
@ -187,7 +181,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(keyCtrl, ctrl_message);
}
// ctrl and sleep monitor might be null, thats okay
return new GBDeviceEvent[]{sendBytesAck, sendBytesCtrl, sleepMonitorResult};
// ctrl might be null, thats okay
return new GBDeviceEvent[]{sendBytesAck, sendBytesCtrl};
}
}

View File

@ -99,7 +99,7 @@ class AppMessageHandlerRealWeather extends AppMessageHandler {
return CLOUD;
} else if (conditionCode >= 907 && conditionCode < 957) {
return STORM;
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
} else if (conditionCode == 905) {
return STORM;
} else if (conditionCode == 900) {
return STORM;

View File

@ -88,7 +88,7 @@ private int getConditionForConditionCode(int conditionCode) {
return HAIL;
} else if (conditionCode >= 907 && conditionCode < 957) {
return WIND;
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
} else if (conditionCode == 905) {
return EXTREME_WIND;
} else if (conditionCode == 900) {
return TORNADO;

View File

@ -130,7 +130,7 @@ class AppMessageHandlerYWeather extends AppMessageHandler {
return SLEET;
} else if (conditionCode >= 907 && conditionCode < 957) {
return STORM;
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
} else if (conditionCode == 905) {
return STORM;
} else if (conditionCode == 900) {
return STORM;

View File

@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class PebbleProtocol extends GBDeviceProtocol {
@ -2269,6 +2270,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
dataLogging.tag = datalogSession.tag;
devEvtsDataLogging = new GBDeviceEvent[]{dataLogging, null};
}
if (datalogSession.uuid.equals(UUID_ZERO) && (datalogSession.tag == 81 || datalogSession.tag == 83 || datalogSession.tag == 84)) {
GB.signalActivityDataFinish();
}
mDatalogSessions.remove(id);
}
break;

View File

@ -224,7 +224,7 @@ public class AndroidUtils {
Uri contentUri = FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".screenshot_provider", file);
intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setData(contentUri);
intent.setDataAndType(contentUri,"application/gpx+xml");
context.startActivity(intent);
}
}

View File

@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
@ -228,6 +229,7 @@ public class DeviceHelper {
result.add(new Roidmi3Coordinator());
result.add(new CasioGB6900DeviceCoordinator());
result.add(new BFH16DeviceCoordinator());
result.add(new MijiaLywsd02Coordinator());
return result;
}

View File

@ -42,6 +42,8 @@ import java.nio.ByteOrder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -492,4 +494,9 @@ public class GB {
throw new AssertionError(errorMessage);
}
}
public static void signalActivityDataFinish() {
Intent intent = new Intent(GBApplication.ACTION_NEW_DATA);
LocalBroadcastManager.getInstance(GBApplication.getContext()).sendBroadcast(intent);
}
}

View File

@ -1,7 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:height="24dp"
android:tint="#7E7E7E"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" />
<path
android:fillColor="#000"
android:pathData="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" />
</vector>

View File

@ -1,7 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#7E7E7E"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM21,9h-6v13h-2v-6h-2v6L9,22L9,9L3,9L3,7h18v2z" />
</vector>

Some files were not shown because too many files have changed in this diff Show More