From 5f998d8a95b1b399e7f1f4ab334250008e2e5c5c Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Thu, 22 Aug 2019 21:19:03 +0200 Subject: [PATCH] add stripped down version of weather part of lineage sdk Makes it possible to use the lineage weather provider without binary jar (This is based on 63a590625c6c76f82e5ef43408a52238b2b34e43 of https://github.com/LineageOS/android_lineage-sdk) --- .../weather/ILineageWeatherManager.aidl | 31 + .../weather/IRequestInfoListener.aidl | 30 + ...IWeatherServiceProviderChangeListener.aidl | 22 + .../aidl/lineageos/weather/RequestInfo.aidl | 19 + .../aidl/lineageos/weather/WeatherInfo.aidl | 19 + .../lineageos/weather/WeatherLocation.aidl | 19 + .../IWeatherProviderService.aidl | 28 + .../IWeatherProviderServiceClient.aidl | 25 + .../weatherservice/ServiceRequestResult.aidl | 19 + .../app/LineageContextConstants.java | 31 + app/src/main/java/lineageos/os/Build.java | 32 + app/src/main/java/lineageos/os/Concierge.java | 153 +++++ .../lineageos/providers/WeatherContract.java | 245 +++++++ .../weather/LineageWeatherManager.java | 435 ++++++++++++ .../java/lineageos/weather/RequestInfo.java | 379 +++++++++++ .../java/lineageos/weather/WeatherInfo.java | 642 ++++++++++++++++++ .../lineageos/weather/WeatherLocation.java | 274 ++++++++ .../lineageos/weather/util/WeatherUtils.java | 84 +++ .../weatherservice/ServiceRequest.java | 161 +++++ .../weatherservice/ServiceRequestResult.java | 197 ++++++ .../WeatherProviderService.java | 181 +++++ 21 files changed, 3026 insertions(+) create mode 100644 app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl create mode 100644 app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl create mode 100644 app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl create mode 100644 app/src/main/aidl/lineageos/weather/RequestInfo.aidl create mode 100644 app/src/main/aidl/lineageos/weather/WeatherInfo.aidl create mode 100644 app/src/main/aidl/lineageos/weather/WeatherLocation.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl create mode 100644 app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl create mode 100644 app/src/main/java/lineageos/app/LineageContextConstants.java create mode 100644 app/src/main/java/lineageos/os/Build.java create mode 100644 app/src/main/java/lineageos/os/Concierge.java create mode 100644 app/src/main/java/lineageos/providers/WeatherContract.java create mode 100644 app/src/main/java/lineageos/weather/LineageWeatherManager.java create mode 100644 app/src/main/java/lineageos/weather/RequestInfo.java create mode 100755 app/src/main/java/lineageos/weather/WeatherInfo.java create mode 100644 app/src/main/java/lineageos/weather/WeatherLocation.java create mode 100644 app/src/main/java/lineageos/weather/util/WeatherUtils.java create mode 100644 app/src/main/java/lineageos/weatherservice/ServiceRequest.java create mode 100644 app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java create mode 100644 app/src/main/java/lineageos/weatherservice/WeatherProviderService.java diff --git a/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl b/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl new file mode 100644 index 000000000..d12e8bbd4 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/ILineageWeatherManager.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import lineageos.weather.IWeatherServiceProviderChangeListener; +import lineageos.weather.RequestInfo; + +interface ILineageWeatherManager { + oneway void updateWeather(in RequestInfo info); + oneway void lookupCity(in RequestInfo info); + oneway void registerWeatherServiceProviderChangeListener( + in IWeatherServiceProviderChangeListener listener); + oneway void unregisterWeatherServiceProviderChangeListener( + in IWeatherServiceProviderChangeListener listener); + String getActiveWeatherServiceProviderLabel(); + oneway void cancelRequest(int requestId); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl b/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl new file mode 100644 index 000000000..11916f3cf --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/IRequestInfoListener.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import lineageos.weather.RequestInfo; +import lineageos.weather.WeatherInfo; +import lineageos.weather.WeatherLocation; + +import java.util.List; + +interface IRequestInfoListener { + void onWeatherRequestCompleted(in RequestInfo requestInfo, int status, + in WeatherInfo weatherInfo); + void onLookupCityRequestCompleted(in RequestInfo requestInfo, int status, + in List weatherLocation); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl b/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl new file mode 100644 index 000000000..12ad2ff8f --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/IWeatherServiceProviderChangeListener.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +/** @hide */ +oneway interface IWeatherServiceProviderChangeListener { + void onWeatherServiceProviderChanged(String providerLabel); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/RequestInfo.aidl b/app/src/main/aidl/lineageos/weather/RequestInfo.aidl new file mode 100644 index 000000000..bdc3ecc64 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/RequestInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable RequestInfo; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl b/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl new file mode 100644 index 000000000..16cbb599e --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/WeatherInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanongenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable WeatherInfo; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl b/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl new file mode 100644 index 000000000..d19e8bce7 --- /dev/null +++ b/app/src/main/aidl/lineageos/weather/WeatherLocation.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +parcelable WeatherLocation; \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl new file mode 100644 index 000000000..fb3bc5ec3 --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderService.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import lineageos.weatherservice.IWeatherProviderServiceClient; +import lineageos.weather.RequestInfo; + +interface IWeatherProviderService { + void processWeatherUpdateRequest(in RequestInfo request); + void processCityNameLookupRequest(in RequestInfo request); + void setServiceClient(in IWeatherProviderServiceClient client); + void cancelOngoingRequests(); + void cancelRequest(int requestId); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl new file mode 100644 index 000000000..2f54eddc1 --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/IWeatherProviderServiceClient.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import lineageos.weather.RequestInfo; +import lineageos.weatherservice.ServiceRequestResult; + +interface IWeatherProviderServiceClient { + void setServiceRequestState(in RequestInfo requestInfo, in ServiceRequestResult result, + int state); +} \ No newline at end of file diff --git a/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl b/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl new file mode 100644 index 000000000..ece4f47fc --- /dev/null +++ b/app/src/main/aidl/lineageos/weatherservice/ServiceRequestResult.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +parcelable ServiceRequestResult; \ No newline at end of file diff --git a/app/src/main/java/lineageos/app/LineageContextConstants.java b/app/src/main/java/lineageos/app/LineageContextConstants.java new file mode 100644 index 000000000..a5ab9d3e3 --- /dev/null +++ b/app/src/main/java/lineageos/app/LineageContextConstants.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2015, The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.app; + + +public final class LineageContextConstants { + + private LineageContextConstants() { + // Empty constructor + } + + public static final String LINEAGE_WEATHER_SERVICE = "lineageweather"; + + public static class Features { + public static final String WEATHER_SERVICES = "org.lineageos.weather"; + } +} diff --git a/app/src/main/java/lineageos/os/Build.java b/app/src/main/java/lineageos/os/Build.java new file mode 100644 index 000000000..61d475b7b --- /dev/null +++ b/app/src/main/java/lineageos/os/Build.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.os; + + +public class Build { + public static class LINEAGE_VERSION_CODES { + public static final int APRICOT = 1; + public static final int BOYSENBERRY = 2; + public static final int CANTALOUPE = 3; + public static final int DRAGON_FRUIT = 4; + public static final int ELDERBERRY = 5; + public static final int FIG = 6; + public static final int GUAVA = 7; + public static final int HACKBERRY = 8; + public static final int ILAMA = 9; + } +} diff --git a/app/src/main/java/lineageos/os/Concierge.java b/app/src/main/java/lineageos/os/Concierge.java new file mode 100644 index 000000000..d4f9b4e22 --- /dev/null +++ b/app/src/main/java/lineageos/os/Concierge.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.os; + +import android.os.Parcel; + +import lineageos.os.Build.LINEAGE_VERSION_CODES; + +/** + * Simply, Concierge handles your parcels and makes sure they get marshalled and unmarshalled + * correctly when cross IPC boundaries even when there is a version mismatch between the client + * sdk level and the framework implementation. + * + *

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

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

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

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

Type: TEXT

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

Type: INTEGER

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

Type: TEXT

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

Type: DOUBLE

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

Type: INTEGER

+ * Can be one of the following: + * + */ + public static final String CURRENT_TEMPERATURE_UNIT = "temperature_unit"; + + /** + * The current weather humidity + *

Type: DOUBLE

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

Type: DOUBLE

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

Type: DOUBLE

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

Type: INTEGER

+ * Can be one of the following: + * + */ + public static final String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit"; + + /** + * The timestamp when this weather was reported + *

Type: LONG

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

Type: DOUBLE

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

Type: DOUBLE

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

Type: DOUBLE

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

Type: DOUBLE

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

Type: TEXT

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

The user removed the active weather service provider from the system

+ *

The active weather provider was disabled.

+ * + * @param providerLabel The label as declared on the weather service provider manifest + */ + void onWeatherServiceProviderChanged(String providerLabel); + } +} diff --git a/app/src/main/java/lineageos/weather/RequestInfo.java b/app/src/main/java/lineageos/weather/RequestInfo.java new file mode 100644 index 000000000..a236bef6b --- /dev/null +++ b/app/src/main/java/lineageos/weather/RequestInfo.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.providers.WeatherContract; + +import java.util.UUID; + +/** + * This class holds the information of a request submitted to the active weather provider service + */ +public final class RequestInfo implements Parcelable { + + private Location mLocation; + private String mCityName; + private WeatherLocation mWeatherLocation; + private int mRequestType; + private IRequestInfoListener mListener; + private int mTempUnit; + private String mKey; + private boolean mIsQueryOnly; + + /** + * A request to update the weather data using a geographical {@link android.location.Location} + */ + public static final int TYPE_WEATHER_BY_GEO_LOCATION_REQ = 1; + /** + * A request to update the weather data using a {@link WeatherLocation} + */ + public static final int TYPE_WEATHER_BY_WEATHER_LOCATION_REQ = 2; + + /** + * A request to look up a city name + */ + public static final int TYPE_LOOKUP_CITY_NAME_REQ = 3; + + private RequestInfo() {} + + /* package */ static class Builder { + private Location mLocation; + private String mCityName; + private WeatherLocation mWeatherLocation; + private int mRequestType; + private IRequestInfoListener mListener; + private int mTempUnit = WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT; + private boolean mIsQueryOnly = false; + + public Builder(IRequestInfoListener listener) { + this.mListener = listener; + } + + /** + * Sets the city name and identifies this request as a {@link #TYPE_LOOKUP_CITY_NAME_REQ} + * request. If set, will null out the location and weather location. Attempting to set + * a null city name will get you an IllegalArgumentException + */ + public Builder setCityName(String cityName) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + this.mCityName = cityName; + this.mRequestType = TYPE_LOOKUP_CITY_NAME_REQ; + this.mLocation = null; + this.mWeatherLocation = null; + return this; + } + + /** + * Sets the Location and identifies this request as a + * {@link #TYPE_WEATHER_BY_GEO_LOCATION_REQ}. If set, will null out the city name and + * weather location. Attempting to set a null location will get you an + * IllegalArgumentException + */ + public Builder setLocation(Location location) { + if (location == null) { + throw new IllegalArgumentException("Location can't be null"); + } + this.mLocation = new Location(location); + this.mCityName = null; + this.mWeatherLocation = null; + this.mRequestType = TYPE_WEATHER_BY_GEO_LOCATION_REQ; + return this; + } + + /** + * Sets the weather location and identifies this request as a + * {@link #TYPE_WEATHER_BY_WEATHER_LOCATION_REQ}. If set, will null out the location and + * city name. Attempting to set a null weather location will get you an + * IllegalArgumentException + */ + public Builder setWeatherLocation(WeatherLocation weatherLocation) { + if (weatherLocation == null) { + throw new IllegalArgumentException("WeatherLocation can't be null"); + } + this.mWeatherLocation = weatherLocation; + this.mLocation = null; + this.mCityName = null; + this.mRequestType = TYPE_WEATHER_BY_WEATHER_LOCATION_REQ; + return this; + } + + /** + * Sets the unit in which the temperature will be reported if the request is honored. + * Valid values are: + *
    + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#CELSIUS} + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit#FAHRENHEIT} + *
+ * Any other value will generate an IllegalArgumentException. If the temperature unit is not + * set, the default will be degrees Fahrenheit + * @param unit A valid temperature unit + */ + public Builder setTemperatureUnit(int unit) { + if (!isValidTempUnit(unit)) { + throw new IllegalArgumentException("Invalid temperature unit"); + } + this.mTempUnit = unit; + return this; + } + + /** + * If this is a weather request, marks the request as a query only, meaning that the + * content provider won't be updated after the active weather service has finished + * processing the request. + */ + public Builder queryOnly() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + this.mIsQueryOnly = true; + break; + default: + this.mIsQueryOnly = false; + break; + } + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link RequestInfo} object + * @return {@link RequestInfo} + */ + public RequestInfo build() { + RequestInfo info = new RequestInfo(); + info.mListener = this.mListener; + info.mRequestType = this.mRequestType; + info.mCityName = this.mCityName; + info.mWeatherLocation = this.mWeatherLocation; + info.mLocation = this.mLocation; + info.mTempUnit = this.mTempUnit; + info.mIsQueryOnly = this.mIsQueryOnly; + info.mKey = UUID.randomUUID().toString(); + return info; + } + + private boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } + + } + + private RequestInfo(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mRequestType = parcel.readInt(); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + mLocation = Location.CREATOR.createFromParcel(parcel); + mTempUnit = parcel.readInt(); + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mWeatherLocation = WeatherLocation.CREATOR.createFromParcel(parcel); + mTempUnit = parcel.readInt(); + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + mCityName = parcel.readString(); + break; + } + mIsQueryOnly = (parcel.readInt() == 1); + mListener = IRequestInfoListener.Stub.asInterface(parcel.readStrongBinder()); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + + /** + * @return The request type + */ + public int getRequestType() { + return mRequestType; + } + + /** + * @return the {@link android.location.Location} if this is a request by location, null + * otherwise + */ + public Location getLocation() { + return new Location(mLocation); + } + + /** + * @return the {@link lineageos.weather.WeatherLocation} if this is a request by weather + * location, null otherwise + */ + public WeatherLocation getWeatherLocation() { + return mWeatherLocation; + } + + /** + * @hide + */ + public IRequestInfoListener getRequestListener() { + return mListener; + } + + /** + * @return the city name if this is a lookup request, null otherwise + */ + public String getCityName() { + return mCityName; + } + + /** + * @return the temperature unit if this is a weather request, -1 otherwise + */ + public int getTemperatureUnit() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + return mTempUnit; + default: + return -1; + } + } + + /** + * @return if this is a weather request, whether the request will update the content provider. + * False for other kind of requests + * @hide + */ + public boolean isQueryOnlyWeatherRequest() { + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + return mIsQueryOnly; + default: + return false; + } + } + + public static final Creator CREATOR = new Creator() { + @Override + public RequestInfo createFromParcel(Parcel in) { + return new RequestInfo(in); + } + + @Override + public RequestInfo[] newArray(int size) { + return new RequestInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeInt(mRequestType); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + mLocation.writeToParcel(dest, 0); + dest.writeInt(mTempUnit); + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mWeatherLocation.writeToParcel(dest, 0); + dest.writeInt(mTempUnit); + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + dest.writeString(mCityName); + break; + } + dest.writeInt(mIsQueryOnly == true ? 1 : 0); + dest.writeStrongBinder(mListener.asBinder()); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("{ Request for "); + switch (mRequestType) { + case TYPE_WEATHER_BY_GEO_LOCATION_REQ: + builder.append("Location: ").append(mLocation); + builder.append(" Temp Unit: "); + if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + builder.append("Fahrenheit"); + } else { + builder.append(" Celsius"); + } + break; + case TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + builder.append("WeatherLocation: ").append(mWeatherLocation); + builder.append(" Temp Unit: "); + if (mTempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + builder.append("Fahrenheit"); + } else { + builder.append(" Celsius"); + } + break; + case TYPE_LOOKUP_CITY_NAME_REQ: + builder.append("Lookup City: ").append(mCityName); + break; + } + return builder.append(" }").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + RequestInfo info = (RequestInfo) obj; + return (TextUtils.equals(mKey, info.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weather/WeatherInfo.java b/app/src/main/java/lineageos/weather/WeatherInfo.java new file mode 100755 index 000000000..91d8b3474 --- /dev/null +++ b/app/src/main/java/lineageos/weather/WeatherInfo.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2016 The CyanongenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.providers.WeatherContract; +import lineageos.weatherservice.ServiceRequest; +import lineageos.weatherservice.ServiceRequestResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * This class represents the weather information that a + * {@link lineageos.weatherservice.WeatherProviderService} will use to update the weather content + * provider. A weather provider service will be called by the system to process an update + * request at any time. If the service successfully processes the request, then the weather provider + * service is responsible of calling + * {@link ServiceRequest#complete(ServiceRequestResult)} to notify the + * system that the request was completed and that the weather content provider should be updated + * with the supplied weather information. + */ +public final class WeatherInfo implements Parcelable { + + private String mCity; + private int mConditionCode; + private double mTemperature; + private int mTempUnit; + private double mTodaysHighTemp; + private double mTodaysLowTemp; + private double mHumidity; + private double mWindSpeed; + private double mWindDirection; + private int mWindSpeedUnit; + private long mTimestamp; + private List mForecastList; + private String mKey; + + private WeatherInfo() {} + + /** + * Builder class for {@link WeatherInfo} + */ + public static class Builder { + private String mCity; + private int mConditionCode = WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE; + private double mTemperature; + private int mTempUnit; + private double mTodaysHighTemp = Double.NaN; + private double mTodaysLowTemp = Double.NaN; + private double mHumidity = Double.NaN; + private double mWindSpeed = Double.NaN; + private double mWindDirection = Double.NaN; + private int mWindSpeedUnit = WeatherContract.WeatherColumns.WindSpeedUnit.MPH; + private long mTimestamp = -1; + private List mForecastList = new ArrayList<>(0); + + /** + * @param cityName A valid city name. Attempting to pass null will get you an + * IllegalArgumentException + * @param temperature A valid temperature value. Attempting pass an invalid double value, + * will get you an IllegalArgumentException + * @param tempUnit A valid temperature unit value. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.TempUnit} for + * valid values. Attempting to pass an invalid temperature unit will get you + * an IllegalArgumentException + */ + public Builder(@NonNull String cityName, double temperature, int tempUnit) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + if (Double.isNaN(temperature)) { + throw new IllegalArgumentException("Invalid temperature"); + } + if (!isValidTempUnit(tempUnit)) { + throw new IllegalArgumentException("Invalid temperature unit"); + } + this.mCity = cityName; + this.mTemperature = temperature; + this.mTempUnit = tempUnit; + } + + /** + * @param timeStamp A timestamp indicating when this data was generated. If timestamps is + * not set, then the builder will set it to the time of object creation + * @return The {@link Builder} instance + */ + public Builder setTimestamp(long timeStamp) { + mTimestamp = timeStamp; + return this; + } + + /** + * @param humidity The weather humidity. Attempting to pass an invalid double value will get + * you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setHumidity(double humidity) { + if (Double.isNaN(humidity)) { + throw new IllegalArgumentException("Invalid humidity value"); + } + + mHumidity = humidity; + return this; + } + + /** + * @param windSpeed The wind speed. Attempting to pass an invalid double value will get you + * an IllegalArgumentException + * @param windDirection The wind direction. Attempting to pass an invalid double value will + * get you an IllegalArgumentException + * @param windSpeedUnit A valid wind speed direction unit. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit} + * for valid values. Attempting to pass an invalid speed unit will get + * you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setWind(double windSpeed, double windDirection, int windSpeedUnit) { + if (Double.isNaN(windSpeed)) { + throw new IllegalArgumentException("Invalid wind speed value"); + } + if (Double.isNaN(windDirection)) { + throw new IllegalArgumentException("Invalid wind direction value"); + } + if (!isValidWindSpeedUnit(windSpeedUnit)) { + throw new IllegalArgumentException("Invalid speed unit"); + } + mWindSpeed = windSpeed; + mWindSpeedUnit = windSpeedUnit; + mWindDirection = windDirection; + return this; + } + + /** + * @param conditionCode A valid weather condition code. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode} + * for valid codes. Attempting to pass an invalid code will get you an + * IllegalArgumentException. + * @return The {@link Builder} instance + */ + public Builder setWeatherCondition(int conditionCode) { + if (!isValidWeatherCode(conditionCode)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = conditionCode; + return this; + } + + /** + * @param forecasts A valid array list of {@link DayForecast} objects. Attempting to pass + * null will get you an IllegalArgumentException' + * @return The {@link Builder} instance + */ + public Builder setForecast(@NonNull List forecasts) { + if (forecasts == null) { + throw new IllegalArgumentException("Forecast list can't be null"); + } + mForecastList = forecasts; + return this; + } + + /** + * + * @param todaysHigh Today's high temperature. Attempting to pass an invalid double value + * will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setTodaysHigh(double todaysHigh) { + if (Double.isNaN(todaysHigh)) { + throw new IllegalArgumentException("Invalid temperature value"); + } + mTodaysHighTemp = todaysHigh; + return this; + } + + /** + * @param todaysLow Today's low temperature. Attempting to pass an invalid double value will + * get you an IllegalArgumentException + * @return + */ + public Builder setTodaysLow(double todaysLow) { + if (Double.isNaN(todaysLow)) { + throw new IllegalArgumentException("Invalid temperature value"); + } + mTodaysLowTemp = todaysLow; + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link WeatherInfo} object + * @return {@link WeatherInfo} + */ + public WeatherInfo build() { + WeatherInfo info = new WeatherInfo(); + info.mCity = this.mCity; + info.mConditionCode = this.mConditionCode; + info.mTemperature = this.mTemperature; + info.mTempUnit = this.mTempUnit; + info.mHumidity = this.mHumidity; + info.mWindSpeed = this.mWindSpeed; + info.mWindDirection = this.mWindDirection; + info.mWindSpeedUnit = this.mWindSpeedUnit; + info.mTimestamp = this.mTimestamp == -1 ? System.currentTimeMillis() : this.mTimestamp; + info.mForecastList = this.mForecastList; + info.mTodaysHighTemp = this.mTodaysHighTemp; + info.mTodaysLowTemp = this.mTodaysLowTemp; + info.mKey = UUID.randomUUID().toString(); + return info; + } + + private boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } + + private boolean isValidWindSpeedUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.WindSpeedUnit.KPH: + case WeatherContract.WeatherColumns.WindSpeedUnit.MPH: + return true; + default: + return false; + } + } + } + + + private static boolean isValidWeatherCode(int code) { + if (code < WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MIN + || code > WeatherContract.WeatherColumns.WeatherCode.WEATHER_CODE_MAX) { + if (code != WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE) { + return false; + } + } + return true; + } + + /** + * @return city name + */ + public String getCity() { + return mCity; + } + + /** + * @return An implementation specific weather condition code + */ + public int getConditionCode() { + return mConditionCode; + } + + /** + * @return humidity + */ + public double getHumidity() { + return mHumidity; + } + + /** + * @return time stamp when the request was processed + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * @return wind direction (degrees) + */ + public double getWindDirection() { + return mWindDirection; + } + + /** + * @return wind speed + */ + public double getWindSpeed() { + return mWindSpeed; + } + + /** + * @return wind speed unit + */ + public int getWindSpeedUnit() { + return mWindSpeedUnit; + } + + /** + * @return current temperature + */ + public double getTemperature() { + return mTemperature; + } + + /** + * @return temperature unit + */ + public int getTemperatureUnit() { + return mTempUnit; + } + + /** + * @return today's high temperature + */ + public double getTodaysHigh() { + return mTodaysHighTemp; + } + + /** + * @return today's low temperature + */ + public double getTodaysLow() { + return mTodaysLowTemp; + } + + /** + * @return List of {@link lineageos.weather.WeatherInfo.DayForecast}. This list will contain + * the forecast weather for the upcoming days. If you want to know today's high and low + * temperatures, use {@link WeatherInfo#getTodaysHigh()} and {@link WeatherInfo#getTodaysLow()} + */ + public List getForecasts() { + return new ArrayList<>(mForecastList); + } + + private WeatherInfo(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mCity = parcel.readString(); + mConditionCode = parcel.readInt(); + mTemperature = parcel.readDouble(); + mTempUnit = parcel.readInt(); + mHumidity = parcel.readDouble(); + mWindSpeed = parcel.readDouble(); + mWindDirection = parcel.readDouble(); + mWindSpeedUnit = parcel.readInt(); + mTodaysHighTemp = parcel.readDouble(); + mTodaysLowTemp = parcel.readDouble(); + mTimestamp = parcel.readLong(); + int forecastListSize = parcel.readInt(); + mForecastList = new ArrayList<>(); + while (forecastListSize > 0) { + mForecastList.add(DayForecast.CREATOR.createFromParcel(parcel)); + forecastListSize--; + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeString(mCity); + dest.writeInt(mConditionCode); + dest.writeDouble(mTemperature); + dest.writeInt(mTempUnit); + dest.writeDouble(mHumidity); + dest.writeDouble(mWindSpeed); + dest.writeDouble(mWindDirection); + dest.writeInt(mWindSpeedUnit); + dest.writeDouble(mTodaysHighTemp); + dest.writeDouble(mTodaysLowTemp); + dest.writeLong(mTimestamp); + dest.writeInt(mForecastList.size()); + for (DayForecast dayForecast : mForecastList) { + dayForecast.writeToParcel(dest, 0); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public WeatherInfo createFromParcel(Parcel source) { + return new WeatherInfo(source); + } + + @Override + public WeatherInfo[] newArray(int size) { + return new WeatherInfo[size]; + } + }; + + /** + * This class represents the weather forecast for a given day. Do not add low and high + * temperatures for the current day in this list. Use + * {@link WeatherInfo.Builder#setTodaysHigh(double)} and + * {@link WeatherInfo.Builder#setTodaysLow(double)} instead. + */ + public static class DayForecast implements Parcelable{ + double mLow; + double mHigh; + int mConditionCode; + String mKey; + + private DayForecast() {} + + /** + * Builder class for {@link DayForecast} + */ + public static class Builder { + double mLow = Double.NaN; + double mHigh = Double.NaN; + int mConditionCode; + + /** + * @param conditionCode A valid weather condition code. See + * {@link lineageos.providers.WeatherContract.WeatherColumns.WeatherCode} for valid + * values. Attempting to pass an invalid code will get you an + * IllegalArgumentException + */ + public Builder(int conditionCode) { + if (!isValidWeatherCode(conditionCode)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = conditionCode; + } + + /** + * @param high Forecast high temperature for this day. Attempting to pass an invalid + * double value will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setHigh(double high) { + if (Double.isNaN(high)) { + throw new IllegalArgumentException("Invalid high forecast temperature"); + } + mHigh = high; + return this; + } + + /** + * @param low Forecast low temperate for this day. Attempting to pass an invalid double + * value will get you an IllegalArgumentException + * @return The {@link Builder} instance + */ + public Builder setLow(double low) { + if (Double.isNaN(low)) { + throw new IllegalArgumentException("Invalid low forecast temperature"); + } + mLow = low; + return this; + } + + + /** + * Combine all of the options that have been set and return a new {@link DayForecast} + * object + * @return {@link DayForecast} + */ + public DayForecast build() { + DayForecast forecast = new DayForecast(); + forecast.mLow = this.mLow; + forecast.mHigh = this.mHigh; + forecast.mConditionCode = this.mConditionCode; + forecast.mKey = UUID.randomUUID().toString(); + return forecast; + } + } + + /** + * @return forecasted low temperature + */ + public double getLow() { + return mLow; + } + + /** + * @return not what you think. Returns the forecasted high temperature + */ + public double getHigh() { + return mHigh; + } + + /** + * @return forecasted weather condition code. Implementation specific + */ + public int getConditionCode() { + return mConditionCode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeDouble(mLow); + dest.writeDouble(mHigh); + dest.writeInt(mConditionCode); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public DayForecast createFromParcel(Parcel source) { + return new DayForecast(source); + } + + @Override + public DayForecast[] newArray(int size) { + return new DayForecast[size]; + } + }; + + private DayForecast(Parcel parcel) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(parcel); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readString(); + mLow = parcel.readDouble(); + mHigh = parcel.readDouble(); + mConditionCode = parcel.readInt(); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{Low temp: ").append(mLow) + .append(" High temp: ").append(mHigh) + .append(" Condition code: ").append(mConditionCode) + .append("}").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + DayForecast forecast = (DayForecast) obj; + return (TextUtils.equals(mKey, forecast.mKey)); + } + return false; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder() + .append(" City Name: ").append(mCity) + .append(" Condition Code: ").append(mConditionCode) + .append(" Temperature: ").append(mTemperature) + .append(" Temperature Unit: ").append(mTempUnit) + .append(" Humidity: ").append(mHumidity) + .append(" Wind speed: ").append(mWindSpeed) + .append(" Wind direction: ").append(mWindDirection) + .append(" Wind Speed Unit: ").append(mWindSpeedUnit) + .append(" Today's high temp: ").append(mTodaysHighTemp) + .append(" Today's low temp: ").append(mTodaysLowTemp) + .append(" Timestamp: ").append(mTimestamp).append(" Forecasts: ["); + for (DayForecast dayForecast : mForecastList) { + builder.append(dayForecast.toString()); + } + return builder.append("]}").toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + WeatherInfo info = (WeatherInfo) obj; + return (TextUtils.equals(mKey, info.mKey)); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/lineageos/weather/WeatherLocation.java b/app/src/main/java/lineageos/weather/WeatherLocation.java new file mode 100644 index 000000000..21a6b07ba --- /dev/null +++ b/app/src/main/java/lineageos/weather/WeatherLocation.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather; + +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; + +import java.util.UUID; + +/** + * A class representing a geographical location that a weather service provider can use to + * get weather data from. Each service provider will potentially populate objects of this class + * with different content, so make sure you don't preserve the values when a service provider + * is changed + */ +public final class WeatherLocation implements Parcelable{ + private String mCityId; + private String mCity; + private String mState; + private String mPostal; + private String mCountryId; + private String mCountry; + private String mKey; + + private WeatherLocation() {} + + /** + * Builder class for {@link WeatherLocation} + */ + public static class Builder { + String mCityId = ""; + String mCity = ""; + String mState = ""; + String mPostal = ""; + String mCountryId = ""; + String mCountry = ""; + + /** + * @param cityId An identifier for the city (for example WOEID - Where On Earth IDentifier) + * @param cityName The name of the city + */ + public Builder(String cityId, String cityName) { + if (cityId == null || cityName == null) { + throw new IllegalArgumentException("Illegal to set city id AND city to null"); + } + this.mCityId = cityId; + this.mCity = cityName; + } + + /** + * @param cityName The name of the city + */ + public Builder(String cityName) { + if (cityName == null) { + throw new IllegalArgumentException("City name can't be null"); + } + this.mCity = cityName; + } + + /** + * @param countryId An identifier for the country (for example ISO alpha-2, ISO alpha-3, + * ISO 3166-1 numeric-3, etc) + * @return The {@link Builder} instance + */ + public Builder setCountryId(String countryId) { + if (countryId == null) { + throw new IllegalArgumentException("Country ID can't be null"); + } + this.mCountryId = countryId; + return this; + } + + /** + * @param country The country name + * @return The {@link Builder} instance + */ + public Builder setCountry(String country) { + if (country == null) { + throw new IllegalArgumentException("Country can't be null"); + } + this.mCountry = country; + return this; + } + + /** + * @param postalCode The postal/ZIP code + * @return The {@link Builder} instance + */ + public Builder setPostalCode(String postalCode) { + if (postalCode == null) { + throw new IllegalArgumentException("Postal code/ZIP can't be null"); + } + this.mPostal = postalCode; + return this; + } + + /** + * @param state The state or territory where the city is located + * @return The {@link Builder} instance + */ + public Builder setState(String state) { + if (state == null) { + throw new IllegalArgumentException("State can't be null"); + } + this.mState = state; + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link WeatherLocation} + * object + * @return {@link WeatherLocation} + */ + public WeatherLocation build() { + WeatherLocation weatherLocation = new WeatherLocation(); + weatherLocation.mCityId = this.mCityId; + weatherLocation.mCity = this.mCity; + weatherLocation.mState = this.mState; + weatherLocation.mPostal = this.mPostal; + weatherLocation.mCountryId = this.mCountryId; + weatherLocation.mCountry = this.mCountry; + weatherLocation.mKey = UUID.randomUUID().toString(); + return weatherLocation; + } + } + + /** + * @return The city ID. This method will return an empty string if the city ID was not set + */ + public String getCityId() { + return mCityId; + } + + /** + * @return The city name. This method will return an empty string if the city name was not set + */ + public String getCity() { + return mCity; + } + + /** + * @return The state name. This method will return an empty string if the state was not set + */ + public String getState() { + return mState; + } + + /** + * @return The postal/ZIP code. This method will return an empty string if the postal/ZIP code + * was not set + */ + public String getPostalCode() { + return mPostal; + } + + /** + * @return The country ID. This method will return an empty string if the country ID was not set + */ + public String getCountryId() { + return mCountryId; + } + + /** + * @return The country name. This method will return an empty string if the country ID was not + * set + */ + public String getCountry() { + return mCountry; + } + + private WeatherLocation(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = in.readString(); + mCityId = in.readString(); + mCity = in.readString(); + mState = in.readString(); + mPostal = in.readString(); + mCountryId = in.readString(); + mCountry = in.readString(); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public WeatherLocation createFromParcel(Parcel in) { + return new WeatherLocation(in); + } + + @Override + public WeatherLocation[] newArray(int size) { + return new WeatherLocation[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + dest.writeString(mCityId); + dest.writeString(mCity); + dest.writeString(mState); + dest.writeString(mPostal); + dest.writeString(mCountryId); + dest.writeString(mCountry); + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{ City ID: ").append(mCityId) + .append(" City: ").append(mCity) + .append(" State: ").append(mState) + .append(" Postal/ZIP Code: ").append(mPostal) + .append(" Country Id: ").append(mCountryId) + .append(" Country: ").append(mCountry).append("}") + .toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + WeatherLocation location = (WeatherLocation) obj; + return (TextUtils.equals(mKey, location.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weather/util/WeatherUtils.java b/app/src/main/java/lineageos/weather/util/WeatherUtils.java new file mode 100644 index 000000000..dd8418ffd --- /dev/null +++ b/app/src/main/java/lineageos/weather/util/WeatherUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weather.util; + + +import lineageos.providers.WeatherContract; + +import java.text.DecimalFormat; + +/** + * Helper class to perform operations and formatting of weather data + */ +public class WeatherUtils { + + /** + * Converts a temperature expressed in degrees Celsius to degrees Fahrenheit + * @param celsius temperature in Celsius + * @return the temperature in degrees Fahrenheit + */ + public static double celsiusToFahrenheit(double celsius) { + return ((celsius * (9d/5d)) + 32d); + } + + /** + * Converts a temperature expressed in degrees Fahrenheit to degrees Celsius + * @param fahrenheit temperature in Fahrenheit + * @return the temperature in degrees Celsius + */ + public static double fahrenheitToCelsius(double fahrenheit) { + return ((fahrenheit - 32d) * (5d/9d)); + } + + /** + * Returns a string representation of the temperature and unit supplied. The temperature value + * will be half-even rounded. + * @param temperature the temperature value + * @param tempUnit A valid {@link WeatherContract.WeatherColumns.TempUnit} + * @return A string with the format XX°F or XX°C (where XX is the temperature) + * depending on the temperature unit that was provided or null if an invalid unit is supplied + */ + public static String formatTemperature(double temperature, int tempUnit) { + if (!isValidTempUnit(tempUnit)) return null; + if (Double.isNaN(temperature)) return "-"; + + DecimalFormat noDigitsFormat = new DecimalFormat("0"); + String noDigitsTemp = noDigitsFormat.format(temperature); + if (noDigitsTemp.equals("-0")) { + noDigitsTemp = "0"; + } + + StringBuilder formatted = new StringBuilder() + .append(noDigitsTemp).append("\u00b0"); + if (tempUnit == WeatherContract.WeatherColumns.TempUnit.CELSIUS) { + formatted.append("C"); + } else if (tempUnit == WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT) { + formatted.append("F"); + } + return formatted.toString(); + } + + private static boolean isValidTempUnit(int unit) { + switch (unit) { + case WeatherContract.WeatherColumns.TempUnit.CELSIUS: + case WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT: + return true; + default: + return false; + } + } +} diff --git a/app/src/main/java/lineageos/weatherservice/ServiceRequest.java b/app/src/main/java/lineageos/weatherservice/ServiceRequest.java new file mode 100644 index 000000000..7f45e5686 --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/ServiceRequest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + + +import android.os.RemoteException; + +import androidx.annotation.NonNull; + +import lineageos.weatherservice.IWeatherProviderServiceClient; +import lineageos.weather.LineageWeatherManager; +import lineageos.weather.RequestInfo; + +/** + * This class represents a request submitted by the system to the active weather provider service + */ +public final class ServiceRequest { + + private final RequestInfo mInfo; + private final IWeatherProviderServiceClient mClient; + + private enum Status { + IN_PROGRESS, COMPLETED, CANCELLED, FAILED, REJECTED + } + private Status mStatus; + + /* package */ ServiceRequest(RequestInfo info, IWeatherProviderServiceClient client) { + mInfo = info; + mClient = client; + mStatus = Status.IN_PROGRESS; + } + + /** + * Obtains the request information + * @return {@link lineageos.weather.RequestInfo} + */ + public RequestInfo getRequestInfo() { + return mInfo; + } + + /** + * This method should be called once the request has been completed + */ + public void complete(@NonNull ServiceRequestResult result) { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + if (result.getWeatherInfo() == null) { + throw new IllegalStateException("The service request result doesn't" + + " contain a valid WeatherInfo object"); + } + mClient.setServiceRequestState(mInfo, result, + LineageWeatherManager.RequestStatus.COMPLETED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + if (result.getLocationLookupList() == null + || result.getLocationLookupList().size() <= 0) { + //In case the user decided to mark this request as completed with + //null or empty list. It's not necessarily a failure + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.NO_MATCH_FOUND); + } else { + mClient.setServiceRequestState(mInfo, result, + LineageWeatherManager.RequestStatus.COMPLETED); + } + break; + } + } catch (RemoteException e) { + } + mStatus = Status.COMPLETED; + } + } + } + + /** + * This method should be called if the service failed to process the request + * (no internet connection, time out, etc.) + */ + public void fail() { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ: + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.FAILED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + mClient.setServiceRequestState(mInfo, null, + LineageWeatherManager.RequestStatus.FAILED); + break; + } + } catch (RemoteException e) { + } + mStatus = Status.FAILED; + } + } + } + + /** + * This method should be called if the service decides not to honor the request. Note this + * method will accept only the following values. + *
    + *
  • {@link lineageos.weather.LineageWeatherManager.RequestStatus#SUBMITTED_TOO_SOON}
  • + *
  • {@link lineageos.weather.LineageWeatherManager.RequestStatus#ALREADY_IN_PROGRESS}
  • + *
+ * Attempting to pass any other value will get you an IllegalArgumentException + * @param status + */ + public void reject(int status) { + synchronized (this) { + if (mStatus.equals(Status.IN_PROGRESS)) { + switch (status) { + case LineageWeatherManager.RequestStatus.ALREADY_IN_PROGRESS: + case LineageWeatherManager.RequestStatus.SUBMITTED_TOO_SOON: + try { + mClient.setServiceRequestState(mInfo, null, status); + } catch (RemoteException e) { + e.printStackTrace(); + } + break; + default: + throw new IllegalArgumentException("Can't reject with status " + status); + } + mStatus = Status.REJECTED; + } + } + } + + /** + * Called by the WeatherProviderService base class to notify we don't want this request anymore. + * The service implementing the WeatherProviderService will be notified of this action + * via onRequestCancelled() + * @hide + */ + public void cancel() { + synchronized (this) { + mStatus = Status.CANCELLED; + } + } +} diff --git a/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java b/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java new file mode 100644 index 000000000..cb1b2a0fc --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/ServiceRequestResult.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import android.os.Parcel; +import android.os.Parcelable; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import lineageos.os.Build; +import lineageos.os.Concierge; +import lineageos.os.Concierge.ParcelInfo; +import lineageos.weather.WeatherLocation; +import lineageos.weather.WeatherInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Use this class to build a request result. + */ +public final class ServiceRequestResult implements Parcelable { + + private WeatherInfo mWeatherInfo; + private List mLocationLookupList; + private String mKey; + + private ServiceRequestResult() {} + + private ServiceRequestResult(Parcel in) { + // Read parcelable version via the Concierge + ParcelInfo parcelInfo = Concierge.receiveParcel(in); + int parcelableVersion = parcelInfo.getParcelVersion(); + + if (parcelableVersion >= Build.LINEAGE_VERSION_CODES.ELDERBERRY) { + mKey = in.readString(); + int hasWeatherInfo = in.readInt(); + if (hasWeatherInfo == 1) { + mWeatherInfo = WeatherInfo.CREATOR.createFromParcel(in); + } + int hasLocationLookupList = in.readInt(); + if (hasLocationLookupList == 1) { + mLocationLookupList = new ArrayList<>(); + int listSize = in.readInt(); + while (listSize > 0) { + mLocationLookupList.add(WeatherLocation.CREATOR.createFromParcel(in)); + listSize--; + } + } + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + public static final Creator CREATOR + = new Creator() { + @Override + public ServiceRequestResult createFromParcel(Parcel in) { + return new ServiceRequestResult(in); + } + + @Override + public ServiceRequestResult[] newArray(int size) { + return new ServiceRequestResult[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Tell the concierge to prepare the parcel + ParcelInfo parcelInfo = Concierge.prepareParcel(dest); + + // ==== ELDERBERRY ===== + dest.writeString(mKey); + if (mWeatherInfo != null) { + dest.writeInt(1); + mWeatherInfo.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mLocationLookupList != null) { + dest.writeInt(1); + dest.writeInt(mLocationLookupList.size()); + for (WeatherLocation lookup : mLocationLookupList) { + lookup.writeToParcel(dest, 0); + } + } else { + dest.writeInt(0); + } + + // Complete parcel info for the concierge + parcelInfo.complete(); + } + + /** + * Builder class for {@link ServiceRequestResult} + */ + public static class Builder { + private WeatherInfo mWeatherInfo; + private List mLocationLookupList; + public Builder() { + this.mWeatherInfo = null; + this.mLocationLookupList = null; + } + + /** + * @param weatherInfo The WeatherInfo object holding the data that will be used to update + * the weather content provider + */ + public Builder(@NonNull WeatherInfo weatherInfo) { + if (weatherInfo == null) { + throw new IllegalArgumentException("WeatherInfo can't be null"); + } + + mWeatherInfo = weatherInfo; + } + + /** + * @param locations The list of WeatherLocation objects. The list should not be null + */ + public Builder(@NonNull List locations) { + if (locations == null) { + throw new IllegalArgumentException("Weather location list can't be null"); + } + mLocationLookupList = locations; + } + + /** + * Creates a {@link ServiceRequestResult} with the arguments + * supplied to this builder + * @return {@link ServiceRequestResult} + */ + public ServiceRequestResult build() { + ServiceRequestResult result = new ServiceRequestResult(); + result.mWeatherInfo = this.mWeatherInfo; + result.mLocationLookupList = this.mLocationLookupList; + result.mKey = UUID.randomUUID().toString(); + return result; + } + } + + /** + * @return The WeatherInfo object supplied by the weather provider service + */ + public WeatherInfo getWeatherInfo() { + return mWeatherInfo; + } + + /** + * @return The list of WeatherLocation objects supplied by the weather provider service + */ + public List getLocationLookupList() { + return new ArrayList<>(mLocationLookupList); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey != null) ? mKey.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + + if (getClass() == obj.getClass()) { + ServiceRequestResult request = (ServiceRequestResult) obj; + return (TextUtils.equals(mKey, request.mKey)); + } + return false; + } +} diff --git a/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java b/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java new file mode 100644 index 000000000..2e3a53539 --- /dev/null +++ b/app/src/main/java/lineageos/weatherservice/WeatherProviderService.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lineageos.weatherservice; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import lineageos.weather.RequestInfo; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + + +public abstract class WeatherProviderService extends Service { + + private Handler mHandler; + private IWeatherProviderServiceClient mClient; + private Set mWeakRequestsSet + = Collections.newSetFromMap(new WeakHashMap()); + + /** + * The {@link android.content.Intent} action that must be declared as handled by a service in + * its manifest for the system to recognize it as a weather provider service + */ + public static final String SERVICE_INTERFACE + = "lineageos.weatherservice.WeatherProviderService"; + + /** + * Name under which a {@link WeatherProviderService} publishes information about itself. + * This meta-data must reference an XML resource containing + * a <weather-provider-service> + * tag. + */ + public static final String SERVICE_META_DATA = "lineageos.weatherservice"; + + @Override + protected final void attachBaseContext(Context base) { + super.attachBaseContext(base); + mHandler = new ServiceHandler(base.getMainLooper()); + } + + @Override + public final IBinder onBind(Intent intent) { + return mBinder; + } + + private final IWeatherProviderService.Stub mBinder = new IWeatherProviderService.Stub() { + + @Override + public void processWeatherUpdateRequest(final RequestInfo info) { + mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget(); + } + + @Override + public void processCityNameLookupRequest(final RequestInfo info) { + mHandler.obtainMessage(ServiceHandler.MSG_ON_NEW_REQUEST, info).sendToTarget(); + } + + @Override + public void setServiceClient(IWeatherProviderServiceClient client) { + mHandler.obtainMessage(ServiceHandler.MSG_SET_CLIENT, client).sendToTarget(); + } + + @Override + public void cancelOngoingRequests() { + synchronized (mWeakRequestsSet) { + for (final ServiceRequest request : mWeakRequestsSet) { + request.cancel(); + mWeakRequestsSet.remove(request); + mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request) + .sendToTarget(); + } + } + } + + @Override + public void cancelRequest(int requestId) { + synchronized (mWeakRequestsSet) { + for (final ServiceRequest request : mWeakRequestsSet) { + if (request.getRequestInfo().hashCode() == requestId) { + mWeakRequestsSet.remove(request); + request.cancel(); + mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_REQUEST, request) + .sendToTarget(); + break; + } + } + } + } + }; + + private class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + public static final int MSG_SET_CLIENT = 1; + public static final int MSG_ON_NEW_REQUEST = 2; + public static final int MSG_CANCEL_REQUEST = 3; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_CLIENT: { + mClient = (IWeatherProviderServiceClient) msg.obj; + if (mClient != null) { + onConnected(); + } else { + onDisconnected(); + } + return; + } + case MSG_ON_NEW_REQUEST: { + RequestInfo info = (RequestInfo) msg.obj; + if (info != null) { + ServiceRequest request = new ServiceRequest(info, mClient); + synchronized (mWeakRequestsSet) { + mWeakRequestsSet.add(request); + } + onRequestSubmitted(request); + } + return; + } + case MSG_CANCEL_REQUEST: { + ServiceRequest request = (ServiceRequest) msg.obj; + onRequestCancelled(request); + return; + } + } + } + } + + /** + * The system has connected to this service. + */ + protected void onConnected() { + /* Do nothing */ + } + + /** + * The system has disconnected from this service. + */ + protected void onDisconnected() { + /* Do nothing */ + } + + /** + * A new request has been submitted to this service + * @param request The service request to be processed by this service + */ + protected abstract void onRequestSubmitted(ServiceRequest request); + + /** + * Called when the system is not interested on this request anymore. Note that the service + * has marked the request as cancelled and you must stop any ongoing operation + * (such as pulling data from internet) that this service could've been performing to honor the + * request. + * + * @param request The request cancelled by the system + */ + protected abstract void onRequestCancelled(ServiceRequest request); +} \ No newline at end of file