diff --git a/Android.mk b/Android.mk index dad01a2..576b339 100644 --- a/Android.mk +++ b/Android.mk @@ -24,7 +24,7 @@ LOCAL_PATH := $(call my-dir) cm_platform_res := APPS/org.cyanogenmod.platform-res_intermediates/src # List of packages used in cm-api-stubs and cm-system-api-stubs -cm_stub_packages := cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.content:cyanogenmod.externalviews:cyanogenmod.hardware:cyanogenmod.media:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.providers:cyanogenmod.platform:cyanogenmod.power:cyanogenmod.themes:cyanogenmod.util +cm_stub_packages := cyanogenmod.alarmclock:cyanogenmod.app:cyanogenmod.content:cyanogenmod.externalviews:cyanogenmod.hardware:cyanogenmod.media:cyanogenmod.os:cyanogenmod.profiles:cyanogenmod.providers:cyanogenmod.platform:cyanogenmod.power:cyanogenmod.themes:cyanogenmod.util:cyanogenmod.weather:cyanogenmod.weatherservice # The CyanogenMod Platform Framework Library # ============================================================ diff --git a/api/cm_current.txt b/api/cm_current.txt index a8b7319..7365d47 100644 --- a/api/cm_current.txt +++ b/api/cm_current.txt @@ -595,6 +595,8 @@ package cyanogenmod.platform { ctor public Manifest.permission(); field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS"; field public static final java.lang.String ACCESS_THEME_MANAGER = "cyanogenmod.permission.ACCESS_THEME_MANAGER"; + field public static final java.lang.String ACCESS_WEATHER_MANAGER = "cyanogenmod.permission.ACCESS_WEATHER_MANAGER"; + field public static final java.lang.String BIND_WEATHER_PROVIDER_SERVICE = "cyanogenmod.permission.BIND_WEATHER_PROVIDER_SERVICE"; field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS"; field public static final java.lang.String LIVE_LOCK_SCREEN_MANAGER_ACCESS = "cyanogenmod.permission.LIVE_LOCK_SCREEN_MANAGER_ACCESS"; field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS"; @@ -609,11 +611,13 @@ package cyanogenmod.platform { field public static final java.lang.String READ_ALARMS = "cyanogenmod.permission.READ_ALARMS"; field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE"; field public static final java.lang.String READ_THEMES = "cyanogenmod.permission.READ_THEMES"; + field public static final java.lang.String READ_WEATHER = "cyanogenmod.permission.READ_WEATHER"; field public static final java.lang.String THIRD_PARTY_KEYGUARD = "android.permission.THIRD_PARTY_KEYGUARD"; field public static final java.lang.String WRITE_ALARMS = "cyanogenmod.permission.WRITE_ALARMS"; field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS"; field public static final java.lang.String WRITE_SETTINGS = "cyanogenmod.permission.WRITE_SETTINGS"; field public static final java.lang.String WRITE_THEMES = "cyanogenmod.permission.WRITE_THEMES"; + field public static final java.lang.String WRITE_WEATHER = "cyanogenmod.permission.WRITE_WEATHER"; } public final class R { @@ -1043,6 +1047,94 @@ package cyanogenmod.providers { field public static final int UPDATING = 2; // 0x2 } + public class WeatherContract { + ctor public WeatherContract(); + field public static final java.lang.String AUTHORITY = "com.cyanogenmod.weather"; + field public static final android.net.Uri AUTHORITY_URI; + } + + public static class WeatherContract.WeatherColumns { + ctor public WeatherContract.WeatherColumns(); + field public static final android.net.Uri CONTENT_URI; + field public static final android.net.Uri CURRENT_AND_FORECAST_WEATHER_URI; + field public static final java.lang.String CURRENT_CITY = "city"; + field public static final java.lang.String CURRENT_CITY_ID = "city_id"; + field public static final java.lang.String CURRENT_CONDITION = "condition"; + field public static final java.lang.String CURRENT_CONDITION_CODE = "condition_code"; + field public static final java.lang.String CURRENT_HUMIDITY = "humidity"; + field public static final java.lang.String CURRENT_TEMPERATURE = "temperature"; + field public static final java.lang.String CURRENT_TEMPERATURE_UNIT = "temperature_unit"; + field public static final java.lang.String CURRENT_TIMESTAMP = "timestamp"; + field public static final android.net.Uri CURRENT_WEATHER_URI; + field public static final java.lang.String CURRENT_WIND_DIRECTION = "wind_direction"; + field public static final java.lang.String CURRENT_WIND_SPEED = "wind_speed"; + field public static final java.lang.String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit"; + field public static final java.lang.String FORECAST_CONDITION = "forecast_condition"; + field public static final java.lang.String FORECAST_CONDITION_CODE = "forecast_condition_code"; + field public static final java.lang.String FORECAST_HIGH = "forecast_high"; + field public static final java.lang.String FORECAST_LOW = "forecast_low"; + field public static final android.net.Uri FORECAST_WEATHER_URI; + } + + public static final class WeatherContract.WeatherColumns.TempUnit { + field public static final int CELSIUS = 1; // 0x1 + field public static final int FAHRENHEIT = 2; // 0x2 + } + + public static final class WeatherContract.WeatherColumns.WeatherCode { + field public static final int BLOWING_SNOW = 14; // 0xe + field public static final int BLUSTERY = 22; // 0x16 + field public static final int CLEAR_NIGHT = 30; // 0x1e + field public static final int CLOUDY = 25; // 0x19 + field public static final int COLD = 24; // 0x18 + field public static final int DRIZZLE = 9; // 0x9 + field public static final int DUST = 18; // 0x12 + field public static final int FAIR_DAY = 33; // 0x21 + field public static final int FAIR_NIGHT = 32; // 0x20 + field public static final int FOGGY = 19; // 0x13 + field public static final int FREEZING_DRIZZLE = 8; // 0x8 + field public static final int FREEZING_RAIN = 10; // 0xa + field public static final int HAIL = 16; // 0x10 + field public static final int HAZE = 20; // 0x14 + field public static final int HEAVY_SNOW = 39; // 0x27 + field public static final int HOT = 35; // 0x23 + field public static final int HURRICANE = 2; // 0x2 + field public static final int ISOLATED_THUNDERSHOWERS = 44; // 0x2c + field public static final int ISOLATED_THUNDERSTORMS = 36; // 0x24 + field public static final int LIGHT_SNOW_SHOWERS = 13; // 0xd + field public static final int MIXED_RAIN_AND_HAIL = 34; // 0x22 + field public static final int MIXED_RAIN_AND_SLEET = 6; // 0x6 + field public static final int MIXED_RAIN_AND_SNOW = 5; // 0x5 + field public static final int MIXED_SNOW_AND_SLEET = 7; // 0x7 + field public static final int MOSTLY_CLOUDY_DAY = 27; // 0x1b + field public static final int MOSTLY_CLOUDY_NIGHT = 26; // 0x1a + field public static final int NOT_AVAILABLE = 3200; // 0xc80 + field public static final int PARTLY_CLOUDY = 41; // 0x29 + field public static final int PARTLY_CLOUDY_DAY = 29; // 0x1d + field public static final int PARTLY_CLOUDY_NIGHT = 28; // 0x1c + field public static final int SCATTERED_SHOWERS = 38; // 0x26 + field public static final int SCATTERED_SNOW_SHOWERS = 40; // 0x28 + field public static final int SCATTERED_THUNDERSTORMS = 37; // 0x25 + field public static final int SEVERE_THUNDERSTORMS = 3; // 0x3 + field public static final int SHOWERS = 11; // 0xb + field public static final int SLEET = 17; // 0x11 + field public static final int SMOKY = 21; // 0x15 + field public static final int SNOW = 15; // 0xf + field public static final int SNOW_FLURRIES = 12; // 0xc + field public static final int SNOW_SHOWERS = 43; // 0x2b + field public static final int SUNNY = 31; // 0x1f + field public static final int THUNDERSHOWER = 42; // 0x2a + field public static final int THUNDERSTORMS = 4; // 0x4 + field public static final int TORNADO = 0; // 0x0 + field public static final int TROPICAL_STORM = 1; // 0x1 + field public static final int WINDY = 23; // 0x17 + } + + public static final class WeatherContract.WeatherColumns.WindSpeedUnit { + field public static final int KPH = 1; // 0x1 + field public static final int MPH = 2; // 0x2 + } + } package cyanogenmod.themes { @@ -1155,3 +1247,147 @@ package cyanogenmod.util { } +package cyanogenmod.weather { + + public class CMWeatherManager { + method public java.lang.String getActiveWeatherServiceProviderLabel(); + method public static cyanogenmod.weather.CMWeatherManager getInstance(android.content.Context); + method public void lookupCity(java.lang.String, cyanogenmod.weather.CMWeatherManager.LookupCityRequestListener); + method public void registerWeatherServiceProviderChangeListener(cyanogenmod.weather.CMWeatherManager.WeatherServiceProviderChangeListener); + method public void requestWeatherUpdate(android.location.Location, cyanogenmod.weather.CMWeatherManager.WeatherUpdateRequestListener); + method public void requestWeatherUpdate(cyanogenmod.weather.WeatherLocation, cyanogenmod.weather.CMWeatherManager.WeatherUpdateRequestListener); + method public void unregisterWeatherServiceProviderChangeListener(cyanogenmod.weather.CMWeatherManager.WeatherServiceProviderChangeListener); + field public static final int WEATHER_REQUEST_ALREADY_IN_PROGRESS = -3; // 0xfffffffd + field public static final int WEATHER_REQUEST_COMPLETED = 1; // 0x1 + field public static final int WEATHER_REQUEST_FAILED = -2; // 0xfffffffe + field public static final int WEATHER_REQUEST_SUBMITTED_TOO_SOON = -1; // 0xffffffff + } + + public static abstract interface CMWeatherManager.LookupCityRequestListener { + method public abstract void onLookupCityRequestCompleted(java.util.ArrayList); + } + + public static abstract interface CMWeatherManager.WeatherServiceProviderChangeListener { + method public abstract void onWeatherServiceProviderChanged(java.lang.String); + } + + public static abstract interface CMWeatherManager.WeatherUpdateRequestListener { + method public abstract void onWeatherRequestCompleted(int, cyanogenmod.weather.WeatherInfo); + } + + public final class RequestInfo implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCityName(); + method public android.location.Location getLocation(); + method public int getRequestType(); + method public int getTemperatureUnit(); + method public cyanogenmod.weather.WeatherLocation getWeatherLocation(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int TYPE_GEO_LOCATION_REQ = 1; // 0x1 + field public static final int TYPE_LOOKUP_CITY_NAME_REQ = 3; // 0x3 + field public static final int TYPE_WEATHER_LOCATION_REQ = 2; // 0x2 + } + + public final class WeatherInfo implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCity(); + method public java.lang.String getCityId(); + method public int getConditionCode(); + method public java.util.ArrayList getForecasts(); + method public float getHumidity(); + method public float getTemperature(); + method public int getTemperatureUnit(); + method public long getTimestamp(); + method public float getWindDirection(); + method public float getWindSpeed(); + method public int getWindSpeedUnit(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherInfo.Builder { + ctor public WeatherInfo.Builder(long); + method public cyanogenmod.weather.WeatherInfo build(); + method public cyanogenmod.weather.WeatherInfo.Builder setCity(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherInfo.Builder setForecast(java.util.ArrayList); + method public cyanogenmod.weather.WeatherInfo.Builder setHumidity(float); + method public cyanogenmod.weather.WeatherInfo.Builder setTemperature(float, int); + method public cyanogenmod.weather.WeatherInfo.Builder setWeatherCondition(int); + method public cyanogenmod.weather.WeatherInfo.Builder setWind(float, float, int); + } + + public static class WeatherInfo.DayForecast implements android.os.Parcelable { + method public int describeContents(); + method public int getConditionCode(); + method public float getHigh(); + method public float getLow(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherInfo.DayForecast.Builder { + ctor public WeatherInfo.DayForecast.Builder(); + method public cyanogenmod.weather.WeatherInfo.DayForecast build(); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setHigh(float); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setLow(float); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setWeatherCondition(int); + } + + public final class WeatherLocation implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCity(); + method public java.lang.String getCityId(); + method public java.lang.String getCountry(); + method public java.lang.String getCountryId(); + method public java.lang.String getPostalCode(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherLocation.Builder { + ctor public WeatherLocation.Builder(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherLocation build(); + method public cyanogenmod.weather.WeatherLocation.Builder setCountry(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherLocation.Builder setPostalCode(java.lang.String); + } + +} + +package cyanogenmod.weatherservice { + + public final class ServiceRequest { + method public void complete(cyanogenmod.weatherservice.ServiceRequestResult); + method public void fail(); + method public cyanogenmod.weather.RequestInfo getRequestInfo(); + } + + public final class ServiceRequestResult implements android.os.Parcelable { + method public int describeContents(); + method public java.util.ArrayList getLocationLookupList(); + method public cyanogenmod.weather.WeatherInfo getWeatherInfo(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class ServiceRequestResult.Builder { + ctor public ServiceRequestResult.Builder(); + method public cyanogenmod.weatherservice.ServiceRequestResult build(); + method public cyanogenmod.weatherservice.ServiceRequestResult.Builder setLocationLookupList(java.util.ArrayList); + method public cyanogenmod.weatherservice.ServiceRequestResult.Builder setWeatherInfo(cyanogenmod.weather.WeatherInfo); + } + + public abstract class WeatherProviderService extends android.app.Service { + ctor public WeatherProviderService(); + method protected final void attachBaseContext(android.content.Context); + method public final android.os.IBinder onBind(android.content.Intent); + method protected void onConnected(); + method protected void onDisconnected(); + method protected abstract void onRequestCancelled(cyanogenmod.weatherservice.ServiceRequest); + method protected abstract void onRequestSubmitted(cyanogenmod.weatherservice.ServiceRequest); + field public static final java.lang.String SERVICE_INTERFACE = "cyanogenmod.weatherservice.WeatherProviderService"; + field public static final java.lang.String SERVICE_META_DATA = "cyanogenmod.weatherservice"; + } + +} + diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/CMWeatherManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/CMWeatherManagerService.java new file mode 100644 index 0000000..c5f78d6 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/CMWeatherManagerService.java @@ -0,0 +1,545 @@ +/* + * 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 org.cyanogenmod.platform.internal; + +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Slog; +import com.android.internal.content.PackageMonitor; +import com.android.internal.os.BackgroundThread; +import com.android.server.SystemService; +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.platform.Manifest; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.providers.WeatherContract.WeatherColumns; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.ICMWeatherManager; +import cyanogenmod.weather.IRequestInfoListener; +import cyanogenmod.weather.IWeatherServiceProviderChangeListener; +import cyanogenmod.weather.RequestInfo; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weatherservice.IWeatherProviderService; +import cyanogenmod.weatherservice.IWeatherProviderServiceClient; +import cyanogenmod.weatherservice.ServiceRequestResult; + +import java.util.ArrayList; +import java.util.List; + +public class CMWeatherManagerService extends SystemService{ + + private static final String TAG = CMWeatherManagerService.class.getSimpleName(); + /** + * How long clients will have to wait until a new weather update request can be honored + * TODO Allow weather service providers to specify this threshold + */ + private static final long REQUEST_THRESHOLD_MILLIS = 1000L * 60L * 10L; + + private IWeatherProviderService mWeatherProviderService; + private boolean mIsWeatherProviderServiceBound; + private long mLastWeatherUpdateRequestTimestamp = -REQUEST_THRESHOLD_MILLIS; + private boolean mIsProcessingRequest = false; + private Object mMutex = new Object(); + private Context mContext; + private final RemoteCallbackList mProviderChangeListeners + = new RemoteCallbackList<>(); + private volatile boolean mReconnectedDuePkgModified = false; + + private final IWeatherProviderServiceClient mServiceClient + = new IWeatherProviderServiceClient.Stub() { + @Override + public void setServiceRequestState(RequestInfo requestInfo, + ServiceRequestResult result, int state) { + synchronized (mMutex) { + + if (requestInfo == null) { + //Invalid request info object + mIsProcessingRequest = false; + return; + } + + final IRequestInfoListener listener = requestInfo.getRequestListener(); + final int requestType = requestInfo.getRequestType(); + + switch (requestType) { + case RequestInfo.TYPE_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_LOCATION_REQ: + if (!isValidRequestInfoState(requestType, state)) { + //We received an invalid state, silently disregard the request + mIsProcessingRequest = false; + return; + } + WeatherInfo weatherInfo = null; + if (state == CMWeatherManager.WEATHER_REQUEST_COMPLETED) { + weatherInfo = (result != null) ? result.getWeatherInfo() : null; + if (weatherInfo == null) { + //This should never happen! WEATHER_REQUEST_COMPLETED is set + //only if the weatherinfo object was not null when the request + //was marked as completed + state = CMWeatherManager.WEATHER_REQUEST_FAILED; + } else { + if (!requestInfo.isQueryOnlyWeatherRequest()) { + final long identity = Binder.clearCallingIdentity(); + try { + updateWeatherInfoLocked(weatherInfo); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + } + if (isValidListener(listener)) { + try { + listener.onWeatherRequestCompleted(requestInfo, state, weatherInfo); + } catch (RemoteException e) { + } + } + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + if (isValidListener(listener)) { + try { + //Result might be null if the provider marked the request as failed + listener.onLookupCityRequestCompleted(requestInfo, + result != null ? result.getLocationLookupList() : null); + } catch (RemoteException e) { + } + } + break; + } + mIsProcessingRequest = false; + } + } + }; + + private boolean isValidRequestInfoState(int requestType, int state) { + switch (requestType) { + case RequestInfo.TYPE_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_LOCATION_REQ: + switch (state) { + case CMWeatherManager.WEATHER_REQUEST_COMPLETED: + case CMWeatherManager.WEATHER_REQUEST_SUBMITTED_TOO_SOON: + case CMWeatherManager.WEATHER_REQUEST_FAILED: + case CMWeatherManager.WEATHER_REQUEST_ALREADY_IN_PROGRESS: + return true; + default: + return false; + } + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + switch (state) { + case CMWeatherManager.LOOKUP_REQUEST_COMPLETED: + case CMWeatherManager.LOOKUP_REQUEST_FAILED: + case CMWeatherManager.LOOKUP_REQUEST_NO_MATCH_FOUND: + return true; + default: + return false; + } + default: + return false; + } + } + + private boolean isValidListener(IRequestInfoListener listener) { + return (listener != null && listener.asBinder().pingBinder()); + } + + private void enforcePermission() { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.ACCESS_WEATHER_MANAGER, null); + } + + private final IBinder mService = new ICMWeatherManager.Stub() { + + @Override + public void updateWeather(RequestInfo info) { + enforcePermission(); + processWeatherUpdateRequest(info); + } + + @Override + public void lookupCity(RequestInfo info) { + enforcePermission(); + processCityNameLookupRequest(info); + } + + @Override + public void registerWeatherServiceProviderChangeListener( + IWeatherServiceProviderChangeListener listener) { + enforcePermission(); + mProviderChangeListeners.register(listener); + } + + @Override + public void unregisterWeatherServiceProviderChangeListener( + IWeatherServiceProviderChangeListener listener) { + enforcePermission(); + mProviderChangeListeners.unregister(listener); + } + + @Override + public String getActiveWeatherServiceProviderLabel() { + enforcePermission(); + final long identity = Binder.clearCallingIdentity(); + try { + String enabledProviderService = CMSettings.Secure.getString( + mContext.getContentResolver(), CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + if (enabledProviderService != null) { + return getComponentLabel( + ComponentName.unflattenFromString(enabledProviderService)); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + return null; + } + }; + + private String getComponentLabel(ComponentName componentName) { + final PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent().setComponent(componentName); + ResolveInfo resolveInfo = pm.resolveService(intent, + PackageManager.GET_SERVICES); + if (resolveInfo != null) { + return resolveInfo.loadLabel(pm).toString(); + } + return null; + } + + public CMWeatherManagerService(Context context) { + super(context); + mContext = context; + } + + @Override + public void onStart() { + publishBinderService(CMContextConstants.CM_WEATHER_SERVICE, mService); + registerPackageMonitor(); + registerSettingsObserver(); + } + + @Override + public void onBootPhase(int phase) { + if (phase == PHASE_ACTIVITY_MANAGER_READY) { + bindActiveWeatherProviderService(); + } + } + + private void bindActiveWeatherProviderService() { + String activeProviderService = CMSettings.Secure.getString(mContext.getContentResolver(), + CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + if (activeProviderService != null) { + if (!getContext().bindServiceAsUser(new Intent().setComponent( + ComponentName.unflattenFromString(activeProviderService)), + mWeatherServiceProviderConnection, Context.BIND_AUTO_CREATE, + UserHandle.CURRENT)) { + Slog.w(TAG, "Failed to bind service " + activeProviderService); + } + } + } + + private boolean canProcessWeatherUpdateRequest(RequestInfo info, long currentTimeMillis) { + final IRequestInfoListener listener = info.getRequestListener(); + + if ((mLastWeatherUpdateRequestTimestamp + REQUEST_THRESHOLD_MILLIS) > currentTimeMillis) { + if (listener != null && listener.asBinder().pingBinder()) { + try { + listener.onWeatherRequestCompleted(info, + CMWeatherManager.WEATHER_REQUEST_SUBMITTED_TOO_SOON, null); + } catch (RemoteException e) { + } + } + return false; + } + + if (mIsProcessingRequest) { + if (listener != null && listener.asBinder().pingBinder()) { + try { + listener.onWeatherRequestCompleted(info, + CMWeatherManager.WEATHER_REQUEST_ALREADY_IN_PROGRESS, null); + } catch (RemoteException e) { + } + } + return false; + } + + if (!mIsWeatherProviderServiceBound) { + if (listener != null && listener.asBinder().pingBinder()) { + try { + listener.onWeatherRequestCompleted(info, + CMWeatherManager.WEATHER_REQUEST_FAILED, null); + } catch (RemoteException e) { + } + } + return false; + } + return true; + } + + private synchronized void processWeatherUpdateRequest(RequestInfo info) { + final long currentTimeMillis = SystemClock.elapsedRealtime(); + + if (!canProcessWeatherUpdateRequest(info, currentTimeMillis)) return; + + mLastWeatherUpdateRequestTimestamp = currentTimeMillis; + mIsProcessingRequest = true; + try { + mWeatherProviderService.processWeatherUpdateRequest(info); + } catch (RemoteException e) { + } + } + + private void processCityNameLookupRequest(RequestInfo info) { + if (!mIsWeatherProviderServiceBound) { + final IRequestInfoListener listener = info.getRequestListener(); + if (listener != null && listener.asBinder().pingBinder()) { + try { + listener.onLookupCityRequestCompleted(info, null); + } catch (RemoteException e) { + } + } + return; + } + + try { + mWeatherProviderService.processCityNameLookupRequest(info); + } catch(RemoteException e){ + } + } + + private ServiceConnection mWeatherServiceProviderConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mWeatherProviderService = IWeatherProviderService.Stub.asInterface(service); + mIsWeatherProviderServiceBound = true; + try { + mWeatherProviderService.setServiceClient(mServiceClient); + } catch(RemoteException e) { + } + if (!mReconnectedDuePkgModified) { + notifyProviderChanged(name); + } + mReconnectedDuePkgModified = false; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mWeatherProviderService = null; + mIsWeatherProviderServiceBound = false; + //We can't talk to the current service anyway... + mIsProcessingRequest = false; + Slog.d(TAG, "Connection with " + name.flattenToString() + " has been closed"); + } + }; + + private void notifyProviderChanged(ComponentName name) { + String providerName = null; + if (name != null) { + providerName = getComponentLabel(name); + } + + int N = mProviderChangeListeners.beginBroadcast(); + for (int indx = 0; indx < N; indx++) { + IWeatherServiceProviderChangeListener listener + = mProviderChangeListeners.getBroadcastItem(indx); + try { + listener.onWeatherServiceProviderChanged(providerName); + } catch (RemoteException e){ + } + } + mProviderChangeListeners.finishBroadcast(); + } + + private boolean updateWeatherInfoLocked(WeatherInfo wi) { + final int size = wi.getForecasts().size() + 1; + List contentValuesList = new ArrayList<>(size); + ContentValues contentValues = new ContentValues(); + + contentValues.put(WeatherColumns.CURRENT_CITY_ID, wi.getCityId()); + contentValues.put(WeatherColumns.CURRENT_CITY, wi.getCity()); + contentValues.put(WeatherColumns.CURRENT_CONDITION_CODE, wi.getConditionCode()); + contentValues.put(WeatherColumns.CURRENT_HUMIDITY, wi.getHumidity()); + contentValues.put(WeatherColumns.CURRENT_TEMPERATURE, wi.getTemperature()); + contentValues.put(WeatherColumns.CURRENT_TEMPERATURE_UNIT, wi.getTemperatureUnit()); + contentValues.put(WeatherColumns.CURRENT_TIMESTAMP, wi.getTimestamp()); + contentValues.put(WeatherColumns.CURRENT_WIND_DIRECTION, wi.getWindDirection()); + contentValues.put(WeatherColumns.CURRENT_WIND_SPEED, wi.getWindSpeed()); + contentValues.put(WeatherColumns.CURRENT_WIND_SPEED_UNIT, wi.getWindSpeedUnit()); + contentValuesList.add(contentValues); + + for (WeatherInfo.DayForecast df : wi.getForecasts()) { + contentValues = new ContentValues(); + contentValues.put(WeatherColumns.FORECAST_LOW, df.getLow()); + contentValues.put(WeatherColumns.FORECAST_HIGH, df.getHigh()); + contentValues.put(WeatherColumns.FORECAST_CONDITION_CODE, df.getConditionCode()); + contentValuesList.add(contentValues); + } + + ContentValues[] updateValues = new ContentValues[contentValuesList.size()]; + if (size != getContext().getContentResolver().bulkInsert( + WeatherColumns.CURRENT_AND_FORECAST_WEATHER_URI, + contentValuesList.toArray(updateValues))) { + Slog.w(TAG, "Failed to update the weather content provider"); + return false; + } + return true; + } + + private void registerPackageMonitor() { + PackageMonitor monitor = new PackageMonitor() { + @Override + public void onPackageModified(String packageName) { + String enabledProviderService = CMSettings.Secure.getString( + mContext.getContentResolver(), CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + if (enabledProviderService == null) return; + ComponentName cn = ComponentName.unflattenFromString(enabledProviderService); + if (!TextUtils.equals(packageName, cn.getPackageName())) return; + + if (cn.getPackageName().equals(packageName) && !mIsWeatherProviderServiceBound) { + //We were disconnected because the whole package changed + //(most likely remove->install) + if (!getContext().bindServiceAsUser(new Intent().setComponent(cn), + mWeatherServiceProviderConnection, Context.BIND_AUTO_CREATE, + UserHandle.CURRENT)) { + CMSettings.Secure.putStringForUser( mContext.getContentResolver(), + CMSettings.Secure.WEATHER_PROVIDER_SERVICE, null, + getChangingUserId()); + Slog.w(TAG, "Unable to rebind " + cn.flattenToString() + " after receiving" + + " package modified notification. Settings updated."); + } else { + mReconnectedDuePkgModified = true; + } + } + } + + @Override + public boolean onPackageChanged(String packageName, int uid, String[] components) { + String enabledProviderService = CMSettings.Secure.getString( + mContext.getContentResolver(), CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + if (enabledProviderService == null) return false; + + boolean packageChanged = false; + ComponentName cn = ComponentName.unflattenFromString(enabledProviderService); + for (String component : components) { + if (cn.getPackageName().equals(component)) { + packageChanged = true; + break; + } + } + + if (packageChanged) { + try { + final IPackageManager pm = AppGlobals.getPackageManager(); + final int enabled = pm.getApplicationEnabledSetting(packageName, + getChangingUserId()); + if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { + return false; + } else { + disconnectClient(); + //The package is not enabled so we can't use it anymore + CMSettings.Secure.putStringForUser( + mContext.getContentResolver(), + CMSettings.Secure.WEATHER_PROVIDER_SERVICE, null, + getChangingUserId()); + Slog.w(TAG, "Active provider " + cn.flattenToString() + " disabled"); + notifyProviderChanged(null); + } + } catch (IllegalArgumentException e) { + Slog.d(TAG, "Exception trying to look up app enabled settings ", e); + } catch (RemoteException e) { + // Really? + } + } + return false; + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + String enabledProviderService = CMSettings.Secure.getString( + mContext.getContentResolver(), CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + if (enabledProviderService == null) return; + + ComponentName cn = ComponentName.unflattenFromString(enabledProviderService); + if (!TextUtils.equals(packageName, cn.getPackageName())) return; + + disconnectClient(); + CMSettings.Secure.putStringForUser( + mContext.getContentResolver(), CMSettings.Secure.WEATHER_PROVIDER_SERVICE, + null, getChangingUserId()); + notifyProviderChanged(null); + } + }; + + monitor.register(mContext, BackgroundThread.getHandler().getLooper(), UserHandle.ALL, true); + } + + private void registerSettingsObserver() { + final Uri enabledWeatherProviderServiceUri = CMSettings.Secure.getUriFor( + CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + ContentObserver observer = new ContentObserver(BackgroundThread.getHandler()) { + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + if (enabledWeatherProviderServiceUri.equals(uri)) { + String activeSrvc = CMSettings.Secure.getString(mContext.getContentResolver(), + CMSettings.Secure.WEATHER_PROVIDER_SERVICE); + disconnectClient(); + if (activeSrvc != null) { + ComponentName cn = ComponentName.unflattenFromString(activeSrvc); + getContext().bindServiceAsUser(new Intent().setComponent(cn), + mWeatherServiceProviderConnection, Context.BIND_AUTO_CREATE, + UserHandle.CURRENT); + } + } + } + }; + mContext.getContentResolver().registerContentObserver(enabledWeatherProviderServiceUri, + false, observer, UserHandle.USER_ALL); + } + + private synchronized void disconnectClient() { + if (mIsWeatherProviderServiceBound) { + if (mIsProcessingRequest) { + try { + mWeatherProviderService.cancelOngoingRequests(); + } catch (RemoteException e) { + } + mIsProcessingRequest = false; + } + try { + mWeatherProviderService.setServiceClient(null); + } catch (RemoteException e) { + } + + getContext().unbindService(mWeatherServiceProviderConnection); + mIsWeatherProviderServiceBound = false; + } + } +} diff --git a/cm/res/AndroidManifest.xml b/cm/res/AndroidManifest.xml index 72c4c70..b403d1e 100644 --- a/cm/res/AndroidManifest.xml +++ b/cm/res/AndroidManifest.xml @@ -192,6 +192,33 @@ android:description="@string/permdesc_accessLiveLockScreenServiceProvider" android:protectionLevel="signature|privileged" /> + + + + + + + + + + + + org.cyanogenmod.platform.internal.ThemeManagerService org.cyanogenmod.platform.internal.IconCacheManagerService org.cyanogenmod.platform.internal.LiveLockScreenServiceBroker + org.cyanogenmod.platform.internal.CMWeatherManagerService diff --git a/cm/res/res/values/strings.xml b/cm/res/res/values/strings.xml index 00baff7..a86ca24 100644 --- a/cm/res/res/values/strings.xml +++ b/cm/res/res/values/strings.xml @@ -179,5 +179,13 @@ Allows a service to provide the live lock screen manager service. - + + read weather + Allows an app to read content from the weather provider + update weather provider + Allows an app to update the content of the weather provider + binds as a weather provider service + Allows an app to be identified as a weather provider service + access weather service + Allows an app to access the weather service in the system. Should never be needed for normal apps diff --git a/src/java/cyanogenmod/app/CMContextConstants.java b/src/java/cyanogenmod/app/CMContextConstants.java index 6b2cb23..98171b8 100644 --- a/src/java/cyanogenmod/app/CMContextConstants.java +++ b/src/java/cyanogenmod/app/CMContextConstants.java @@ -119,6 +119,18 @@ public final class CMContextConstants { */ public static final String CM_LIVE_LOCK_SCREEN_SERVICE = "cmlivelockscreen"; + /** + * Use with {@link android.content.Context#getSystemService} to retrieve a + * {@link cyanogenmod.weather.CMWeatherManager} to manage the weather service + * settings and request weather updates + * + * @see android.content.Context#getSystemService + * @see cyanogenmod.weather.CMWeatherManager + * + * @hide + */ + public static final String CM_WEATHER_SERVICE = "cmweather"; + /** * Features supported by the CMSDK. */ @@ -194,5 +206,13 @@ public final class CMContextConstants { */ @SdkConstant(SdkConstant.SdkConstantType.FEATURE) public static final String LIVE_LOCK_SCREEN = "org.cyanogenmod.livelockscreen"; + + /** + * Feature for {@link PackageManager#getSystemAvailableFeatures} and + * {@link PackageManager#hasSystemFeature}: The device includes the cm weather weather + * service utilized by the cmsdk. + */ + @SdkConstant(SdkConstant.SdkConstantType.FEATURE) + public static final String WEATHER_SERVICES = "org.cyanogenmod.weather"; } } diff --git a/src/java/cyanogenmod/providers/CMSettings.java b/src/java/cyanogenmod/providers/CMSettings.java index 3886c5e..1a0f9a9 100644 --- a/src/java/cyanogenmod/providers/CMSettings.java +++ b/src/java/cyanogenmod/providers/CMSettings.java @@ -2686,6 +2686,13 @@ public final class CMSettings { */ public static final String ENABLED_EVENT_LIVE_LOCKS_KEY = "live_lockscreens_events_enabled"; + /** + * Current active & enabled Weather Provider Service + * + * @hide + */ + public static final String WEATHER_PROVIDER_SERVICE = "weather_provider_service"; + // endregion /** diff --git a/src/java/cyanogenmod/providers/WeatherContract.java b/src/java/cyanogenmod/providers/WeatherContract.java new file mode 100644 index 0000000..e8e3726 --- /dev/null +++ b/src/java/cyanogenmod/providers/WeatherContract.java @@ -0,0 +1,240 @@ +/* + * 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 cyanogenmod.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 = "com.cyanogenmod.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"); + + /** + * A unique ID for the city. NOTE: this value fully depends on the implementation of the + * weather provider service and can potentially change when you switch providers. + *

Type: TEXT

+ */ + public static final String CURRENT_CITY_ID = "city_id"; + + /** + * 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: FLOAT

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

Type: INTEGER

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

Type: FLOAT

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

Type: FLOAT

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

Type: FLOAT

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

Type: INTEGER

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

Type: LONG

+ */ + public static final String CURRENT_TIMESTAMP = "timestamp"; + + /** + * The forecasted low temperature + *

Type: FLOAT

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

Type: FLOAT

+ */ + 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/src/java/cyanogenmod/weather/CMWeatherManager.java b/src/java/cyanogenmod/weather/CMWeatherManager.java new file mode 100644 index 0000000..32eab52 --- /dev/null +++ b/src/java/cyanogenmod/weather/CMWeatherManager.java @@ -0,0 +1,373 @@ +/* + * 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 cyanogenmod.weather; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.location.Location; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArraySet; +import cyanogenmod.app.CMContextConstants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Provides access to the weather services in the device. + */ +public class CMWeatherManager { + + private static ICMWeatherManager sWeatherManagerService; + private static CMWeatherManager 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 = CMWeatherManager.class.getSimpleName(); + + /** + * Weather update request state: Successfully completed + */ + public static final int WEATHER_REQUEST_COMPLETED = 1; + + /** + * Weather update request state: You need to wait a bit longer before requesting an update + * again. + *

Please bear in mind that the weather does not change very often. A threshold of 10 minutes + * is enforced by the system

+ */ + public static final int WEATHER_REQUEST_SUBMITTED_TOO_SOON = -1; + + /** + * Weather update request state: An error occurred while trying to update the weather. You + * should wait before trying again, or your request will be rejected with + * {@link #WEATHER_REQUEST_SUBMITTED_TOO_SOON} + */ + public static final int WEATHER_REQUEST_FAILED = -2; + + /** + * Weather update request state: Only one update request can be processed at a given time. + */ + public static final int WEATHER_REQUEST_ALREADY_IN_PROGRESS = -3; + + /** @hide */ + public static final int LOOKUP_REQUEST_COMPLETED = 100; + + /** @hide */ + public static final int LOOKUP_REQUEST_FAILED = -100; + + /** @hide */ + public static final int LOOKUP_REQUEST_NO_MATCH_FOUND = -101; + + + private CMWeatherManager(Context context) { + Context appContext = context.getApplicationContext(); + mContext = (appContext != null) ? appContext : context; + sWeatherManagerService = getService(); + + if (context.getPackageManager().hasSystemFeature( + CMContextConstants.Features.WEATHER_SERVICES) && (sWeatherManagerService == null)) { + throw new RuntimeException("Unable to bind the CMWeatherManagerService"); + } + mHandler = new Handler(appContext.getMainLooper()); + } + + /** + * Gets or creates an instance of the {@link cyanogenmod.weather.CMWeatherManager} + * @param context + * @return {@link CMWeatherManager} + */ + public static CMWeatherManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new CMWeatherManager(context); + } + return sInstance; + } + + /** @hide */ + public static ICMWeatherManager getService() { + if (sWeatherManagerService != null) { + return sWeatherManagerService; + } + IBinder binder = ServiceManager.getService(CMContextConstants.CM_WEATHER_SERVICE); + if (binder != null) { + sWeatherManagerService = ICMWeatherManager.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 + */ + public void requestWeatherUpdate(@NonNull Location location, + @NonNull WeatherUpdateRequestListener listener) { + if (sWeatherManagerService == null) { + return; + } + + try { + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setLocation(location) + .build(); + if (listener != null) mWeatherUpdateRequestListeners.put(info, listener); + sWeatherManagerService.updateWeather(info); + } catch (RemoteException e) { + } + } + + /** + * 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 cyanogenmod.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 + */ + public void requestWeatherUpdate(@NonNull WeatherLocation weatherLocation, + @NonNull WeatherUpdateRequestListener listener) { + if (sWeatherManagerService == null) { + return; + } + + try { + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setWeatherLocation(weatherLocation) + .build(); + if (listener != null) mWeatherUpdateRequestListeners.put(info, listener); + sWeatherManagerService.updateWeather(info); + } catch (RemoteException e) { + } + } + + /** + * 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 cyanogenmod.weather.WeatherLocation} + * will be provided + */ + public void lookupCity(@NonNull String city, @NonNull LookupCityRequestListener listener) { + if (sWeatherManagerService == null) { + return; + } + try { + RequestInfo info = new RequestInfo + .Builder(mRequestInfoListener) + .setCityName(city) + .build(); + if (listener != null) mLookupNameRequestListeners.put(info, listener); + sWeatherManagerService.lookupCity(info); + } 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) { + 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) { + 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() { + 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 state, + final WeatherInfo weatherInfo) { + final WeatherUpdateRequestListener listener + = mWeatherUpdateRequestListeners.remove(requestInfo); + if (listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + listener.onWeatherRequestCompleted(state, weatherInfo); + } + }); + } + } + + @Override + public void onLookupCityRequestCompleted(RequestInfo requestInfo, + final List weatherLocations) { + + final LookupCityRequestListener listener + = mLookupNameRequestListeners.remove(requestInfo); + if (listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + ArrayList list = null; + if (weatherLocations != null) { + list = new ArrayList<>(weatherLocations); + } + listener.onLookupCityRequestCompleted(list); + } + }); + } + } + }; + + /** + * 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 state Any of the following values + * {@link #WEATHER_REQUEST_COMPLETED} + * {@link #WEATHER_REQUEST_ALREADY_IN_PROGRESS} + * {@link #WEATHER_REQUEST_SUBMITTED_TOO_SOON} + * {@link #WEATHER_REQUEST_FAILED} + * + * @param weatherInfo A fully populated {@link WeatherInfo} if state is + * {@link #WEATHER_REQUEST_COMPLETED}, null otherwise + */ + void onWeatherRequestCompleted(int state, 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. The argument can be null if the provider couldn't find a match + * + * @param locations + */ + void onLookupCityRequestCompleted(ArrayList 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/src/java/cyanogenmod/weather/ICMWeatherManager.aidl b/src/java/cyanogenmod/weather/ICMWeatherManager.aidl new file mode 100644 index 0000000..21e1d26 --- /dev/null +++ b/src/java/cyanogenmod/weather/ICMWeatherManager.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 cyanogenmod.weather; + +import cyanogenmod.weather.IWeatherServiceProviderChangeListener; +import cyanogenmod.weather.RequestInfo; + +/** @hide */ +interface ICMWeatherManager { + 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(); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/IRequestInfoListener.aidl b/src/java/cyanogenmod/weather/IRequestInfoListener.aidl new file mode 100644 index 0000000..553da71 --- /dev/null +++ b/src/java/cyanogenmod/weather/IRequestInfoListener.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 cyanogenmod.weather; + +import cyanogenmod.weather.RequestInfo; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weather.WeatherLocation; + +import java.util.List; + +/** @hide */ +oneway interface IRequestInfoListener { + void onWeatherRequestCompleted(in RequestInfo requestInfo, int state, + in WeatherInfo weatherInfo); + void onLookupCityRequestCompleted(in RequestInfo requestInfo, + in List weatherLocation); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/IWeatherServiceProviderChangeListener.aidl b/src/java/cyanogenmod/weather/IWeatherServiceProviderChangeListener.aidl new file mode 100644 index 0000000..7c823b6 --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weather; + +/** @hide */ +oneway interface IWeatherServiceProviderChangeListener { + void onWeatherServiceProviderChanged(String providerLabel); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/RequestInfo.aidl b/src/java/cyanogenmod/weather/RequestInfo.aidl new file mode 100644 index 0000000..5e53b93 --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weather; + +parcelable RequestInfo; \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/RequestInfo.java b/src/java/cyanogenmod/weather/RequestInfo.java new file mode 100644 index 0000000..e99af98 --- /dev/null +++ b/src/java/cyanogenmod/weather/RequestInfo.java @@ -0,0 +1,355 @@ +/* + * 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 cyanogenmod.weather; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; +import cyanogenmod.os.Build; +import cyanogenmod.providers.WeatherContract; + +/** + * 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 int mKey; + private boolean mIsQueryOnly; + + /** + * A request to update the weather data using a geographical {@link android.location.Location} + */ + public static final int TYPE_GEO_LOCATION_REQ = 1; + /** + * A request to update the weather data using a {@link WeatherLocation} + */ + public static final int TYPE_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. + */ + public Builder setCityName(String cityName) { + 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_GEO_LOCATION_REQ}. If + * set, will null out the city name and weather location. + */ + public Builder setLocation(Location location) { + this.mLocation = location; + this.mCityName = null; + this.mWeatherLocation = null; + this.mRequestType = TYPE_GEO_LOCATION_REQ; + return this; + } + + /** + * Sets the weather location and identifies this request as a + * {@link #TYPE_WEATHER_LOCATION_REQ}. If set, will null out the location and city name + */ + public Builder setWeatherLocation(WeatherLocation weatherLocation) { + this.mWeatherLocation = weatherLocation; + this.mLocation = null; + this.mCityName = null; + this.mRequestType = TYPE_WEATHER_LOCATION_REQ; + return this; + } + + /** + * Sets the unit in which the temperature will be reported if the request is honored. + * Valid values are: + *
    + * {@link cyanogenmod.providers.WeatherContract.WeatherColumns.TempUnit#CELSIUS} + * {@link cyanogenmod.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_GEO_LOCATION_REQ: + case TYPE_WEATHER_LOCATION_REQ: + this.mIsQueryOnly = true; + break; + default: + this.mIsQueryOnly = false; + break; + } + return this; + } + + 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 = this.hashCode(); + 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) { + int parcelableVersion = parcel.readInt(); + int parcelableSize = parcel.readInt(); + int startPosition = parcel.dataPosition(); + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readInt(); + mRequestType = parcel.readInt(); + switch (mRequestType) { + case TYPE_GEO_LOCATION_REQ: + mLocation = Location.CREATOR.createFromParcel(parcel); + mTempUnit = parcel.readInt(); + break; + case TYPE_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()); + } + parcel.setDataPosition(startPosition + parcelableSize); + } + + + /** + * @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 mLocation; + } + + /** + * @return the {@link cyanogenmod.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_GEO_LOCATION_REQ: + case TYPE_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_GEO_LOCATION_REQ: + case TYPE_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) { + dest.writeInt(Build.PARCELABLE_VERSION); + + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // ==== ELDERBERRY ===== + dest.writeInt(mKey); + dest.writeInt(mRequestType); + switch (mRequestType) { + case TYPE_GEO_LOCATION_REQ: + mLocation.writeToParcel(dest, 0); + dest.writeInt(mTempUnit); + break; + case TYPE_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()); + + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("{ Request for "); + switch (mRequestType) { + case TYPE_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_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() { + //The hashcode of this object was stored when it was built. This is an + //immutable object but we need to preserve the hashcode since this object is parcelable and + //it's reconstructed over IPC, and clients of this object might want to store it in a + //collection that relies on this code to identify the object + return mKey; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof RequestInfo) { + RequestInfo info = (RequestInfo) obj; + return (info.hashCode() == this.mKey); + } + return false; + } +} diff --git a/src/java/cyanogenmod/weather/WeatherInfo.aidl b/src/java/cyanogenmod/weather/WeatherInfo.aidl new file mode 100644 index 0000000..ffff02c --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weather; + +parcelable WeatherInfo; \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/WeatherInfo.java b/src/java/cyanogenmod/weather/WeatherInfo.java new file mode 100755 index 0000000..3fd2eac --- /dev/null +++ b/src/java/cyanogenmod/weather/WeatherInfo.java @@ -0,0 +1,525 @@ +/* + * 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 cyanogenmod.weather; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import cyanogenmod.os.Build; +import cyanogenmod.providers.WeatherContract; +import cyanogenmod.weatherservice.ServiceRequest; +import cyanogenmod.weatherservice.ServiceRequestResult; + +import java.util.ArrayList; + +/** + * This class represents the weather information that a + * {@link cyanogenmod.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 mCityId; + private String mCity; + private int mConditionCode; + private float mTemperature; + private int mTempUnit; + private float mHumidity; + private float mWindSpeed; + private float mWindDirection; + private int mWindSpeedUnit; + private long mTimestamp; + private ArrayList mForecastList; + int mKey; + + private WeatherInfo() {} + + public static class Builder { + private String mCityId; + private String mCity; + private int mConditionCode; + private float mTemperature; + private int mTempUnit; + private float mHumidity; + private float mWindSpeed; + private float mWindDirection; + private int mWindSpeedUnit; + private long mTimestamp; + private ArrayList mForecastList; + + public Builder(long timestamp) { + mTimestamp = timestamp; + } + + public Builder setCity(String cityId, @NonNull String cityName) { + if (cityName == null || cityId == null) { + throw new IllegalArgumentException("City name and id can't be null"); + } + mCityId = cityId; + mCity = cityName; + return this; + } + + public Builder setTemperature(float temperature, int tempUnit) { + if (!isValidTempUnit(tempUnit)) { + throw new IllegalArgumentException("Invalid temperature unit"); + } + + if (Float.isNaN(temperature)) { + throw new IllegalArgumentException("Invalid temperature value"); + } + + mTemperature = temperature; + mTempUnit = tempUnit; + return this; + } + + public Builder setHumidity(float humidity) { + if (Float.isNaN(humidity)) { + throw new IllegalArgumentException("Invalid humidity value"); + } + + mHumidity = humidity; + return this; + } + + public Builder setWind(float windSpeed, float windDirection, int windSpeedUnit) { + if (Float.isNaN(windSpeed)) { + throw new IllegalArgumentException("Invalid wind speed value"); + } + if (Float.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; + } + + public Builder setWeatherCondition(int conditionCode) { + if (!isValidWeatherCode(conditionCode)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = conditionCode; + return this; + } + + public Builder setForecast(@NonNull ArrayList forecasts) { + if (forecasts == null) { + throw new IllegalArgumentException("Forecast list can't be null"); + } + mForecastList = forecasts; + return this; + } + + public WeatherInfo build() { + WeatherInfo info = new WeatherInfo(); + info.mCityId = this.mCityId; + 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; + info.mForecastList = this.mForecastList; + info.mKey = this.hashCode(); + 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 id + */ + public String getCityId() { + return mCityId; + } + + /** + * @return city name + */ + public String getCity() { + return mCity; + } + + /** + * @return An implementation specific weather condition code + */ + public int getConditionCode() { + return mConditionCode; + } + + /** + * @return humidity + */ + public float getHumidity() { + return mHumidity; + } + + /** + * @return time stamp when the request was processed + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * @return wind direction (degrees) + */ + public float getWindDirection() { + return mWindDirection; + } + + /** + * @return wind speed + */ + public float getWindSpeed() { + return mWindSpeed; + } + + /** + * @return wind speed unit + */ + public int getWindSpeedUnit() { + return mWindSpeedUnit; + } + + /** + * @return current temperature + */ + public float getTemperature() { + return mTemperature; + } + + /** + * @return temperature unit + */ + public int getTemperatureUnit() { + return mTempUnit; + } + + /** + * @return List of {@link cyanogenmod.weather.WeatherInfo.DayForecast} + */ + public ArrayList getForecasts() { + return mForecastList; + } + + private WeatherInfo(Parcel parcel) { + int parcelableVersion = parcel.readInt(); + int parcelableSize = parcel.readInt(); + int startPosition = parcel.dataPosition(); + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readInt(); + mCityId = parcel.readString(); + mCity = parcel.readString(); + mConditionCode = parcel.readInt(); + mTemperature = parcel.readFloat(); + mTempUnit = parcel.readInt(); + mHumidity = parcel.readFloat(); + mWindSpeed = parcel.readFloat(); + mWindDirection = parcel.readFloat(); + mWindSpeedUnit = parcel.readInt(); + mTimestamp = parcel.readLong(); + int forecastListSize = parcel.readInt(); + mForecastList = new ArrayList<>(); + while (forecastListSize > 0) { + mForecastList.add(DayForecast.CREATOR.createFromParcel(parcel)); + forecastListSize--; + } + } + parcel.setDataPosition(startPosition + parcelableSize); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(Build.PARCELABLE_VERSION); + + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // ==== ELDERBERRY ===== + dest.writeInt(mKey); + dest.writeString(mCityId); + dest.writeString(mCity); + dest.writeInt(mConditionCode); + dest.writeFloat(mTemperature); + dest.writeInt(mTempUnit); + dest.writeFloat(mHumidity); + dest.writeFloat(mWindSpeed); + dest.writeFloat(mWindDirection); + dest.writeInt(mWindSpeedUnit); + dest.writeLong(mTimestamp); + dest.writeInt(mForecastList.size()); + for (DayForecast dayForecast : mForecastList) { + dayForecast.writeToParcel(dest, 0); + } + + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + 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 + */ + public static class DayForecast implements Parcelable{ + float mLow; + float mHigh; + int mConditionCode; + int mKey; + + private DayForecast() {} + + public static class Builder { + float mLow; + float mHigh; + int mConditionCode; + + public Builder() {} + public Builder setHigh(float high) { + if (Float.isNaN(high)) { + throw new IllegalArgumentException("Invalid high forecast temperature"); + } + mHigh = high; + return this; + } + public Builder setLow(float low) { + if (Float.isNaN(low)) { + throw new IllegalArgumentException("Invalid low forecast temperature"); + } + mLow = low; + return this; + } + + public Builder setWeatherCondition(int code) { + if (!isValidWeatherCode(code)) { + throw new IllegalArgumentException("Invalid weather condition code"); + } + mConditionCode = code; + return this; + } + + public DayForecast build() { + DayForecast forecast = new DayForecast(); + forecast.mLow = this.mLow; + forecast.mHigh = this.mHigh; + forecast.mConditionCode = this.mConditionCode; + forecast.mKey = this.hashCode(); + return forecast; + } + } + + /** + * @return forecasted low temperature + */ + public float getLow() { + return mLow; + } + + /** + * @return not what you think. Returns the forecasted high temperature + */ + public float 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) { + dest.writeInt(Build.PARCELABLE_VERSION); + + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // ==== ELDERBERRY ===== + dest.writeInt(mKey); + dest.writeFloat(mLow); + dest.writeFloat(mHigh); + dest.writeInt(mConditionCode); + + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + 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) { + int parcelableVersion = parcel.readInt(); + int parcelableSize = parcel.readInt(); + int startPosition = parcel.dataPosition(); + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mKey = parcel.readInt(); + mLow = parcel.readFloat(); + mHigh = parcel.readFloat(); + mConditionCode = parcel.readInt(); + } + parcel.setDataPosition(startPosition + parcelableSize); + } + + @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() { + //The hashcode of this object was stored when it was built. This is an + //immutable object but we need to preserve the hashcode since this object is parcelable + //and it's reconstructed over IPC, and clients of this object might want to store it in + //a collection that relies on this code to identify the object + return mKey; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DayForecast) { + DayForecast forecast = (DayForecast) obj; + return (forecast.hashCode() == this.mKey); + } + return false; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder() + .append("{CityId: ").append(mCityId) + .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(" Timestamp: ").append(mTimestamp).append(" Forecasts: ["); + for (DayForecast dayForecast : mForecastList) { + builder.append(dayForecast.toString()); + } + return builder.append("]}").toString(); + } + + @Override + public int hashCode() { + //The hashcode of this object was stored when it was built. This is an + //immutable object but we need to preserve the hashcode since this object is parcelable and + //it's reconstructed over IPC, and clients of this object might want to store it in a + //collection that relies on this code to identify the object + return mKey; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WeatherInfo) { + WeatherInfo info = (WeatherInfo) obj; + return (info.hashCode() == this.mKey); + } + return false; + } +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/WeatherLocation.aidl b/src/java/cyanogenmod/weather/WeatherLocation.aidl new file mode 100644 index 0000000..23f7b24 --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weather; + +parcelable WeatherLocation; \ No newline at end of file diff --git a/src/java/cyanogenmod/weather/WeatherLocation.java b/src/java/cyanogenmod/weather/WeatherLocation.java new file mode 100644 index 0000000..b7827e9 --- /dev/null +++ b/src/java/cyanogenmod/weather/WeatherLocation.java @@ -0,0 +1,172 @@ +/* + * 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 cyanogenmod.weather; + +import android.os.Parcel; +import android.os.Parcelable; +import cyanogenmod.os.Build; + +/** + * 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 mPostal; + private String mCountryId; + private String mCountry; + private int mKey; + + private WeatherLocation() {} + + public static class Builder { + String mCityId; + String mCity; + String mPostal; + String mCountryId; + String mCountry; + + public Builder(String cityId, String cityName) { + this.mCityId = cityId; + this.mCity = cityName; + } + + public Builder setCountry(String countyId, String country) { + this.mCountryId = countyId; + this.mCountry = country; + return this; + } + + public Builder setPostalCode(String postalCode) { + this.mPostal = postalCode; + return this; + } + + public WeatherLocation build() { + WeatherLocation weatherLocation = new WeatherLocation(); + weatherLocation.mCityId = this.mCityId; + weatherLocation.mCity = this.mCity; + weatherLocation.mPostal = this.mPostal; + weatherLocation.mCountryId = this.mCountryId; + weatherLocation.mCountry = this.mCountry; + weatherLocation.mKey = this.hashCode(); + return weatherLocation; + } + } + + public String getCityId() { + return mCityId; + } + + public String getCity() { + return mCity; + } + + public String getPostalCode() { + return mPostal; + } + + public String getCountryId() { + return mCountryId; + } + + public String getCountry() { + return mCountry; + } + + private WeatherLocation(Parcel in) { + int parcelableVersion = in.readInt(); + int parcelableSize = in.readInt(); + int startPosition = in.dataPosition(); + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mKey = in.readInt(); + mCityId = in.readString(); + mCity = in.readString(); + mPostal = in.readString(); + mCountryId = in.readString(); + mCountry = in.readString(); + } + in.setDataPosition(startPosition + parcelableSize); + } + + 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) { + dest.writeInt(Build.PARCELABLE_VERSION); + + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // ==== ELDERBERRY ===== + dest.writeInt(mKey); + dest.writeString(mCityId); + dest.writeString(mCity); + dest.writeString(mPostal); + dest.writeString(mCountryId); + dest.writeString(mCountry); + + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + @Override + public String toString() { + return new StringBuilder() + .append("{ City ID: ").append(mCityId) + .append(" City: ").append(mCity) + .append(" Postal Code: ").append(mPostal) + .append(" Country Id: ").append(mCountryId) + .append(" Country: ").append(mCountry).append("}") + .toString(); + } + + @Override + public int hashCode() { + return mKey; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof WeatherLocation) { + WeatherLocation info = (WeatherLocation) obj; + return (info.hashCode() == this.mKey); + } + return false; + } +} diff --git a/src/java/cyanogenmod/weather/util/WeatherUtils.java b/src/java/cyanogenmod/weather/util/WeatherUtils.java new file mode 100644 index 0000000..c89213b --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weather.util; + + +import cyanogenmod.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 float celsiusToFahrenheit(float celsius) { + return ((celsius * (9f/5f)) + 32f); + } + + /** + * Converts a temperature expressed in degrees Fahrenheit to degrees Celsius + * @param fahrenheit temperature in Fahrenheit + * @return the temperature in degrees Celsius + */ + public static float fahrenheitToCelsius(float fahrenheit) { + return ((fahrenheit - 32f) * (5f/9f)); + } + + /** + * 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(float temperature, int tempUnit) { + if (!isValidTempUnit(tempUnit)) return null; + if (Float.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/src/java/cyanogenmod/weatherservice/IWeatherProviderService.aidl b/src/java/cyanogenmod/weatherservice/IWeatherProviderService.aidl new file mode 100644 index 0000000..d9eceb3 --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weatherservice; + +import cyanogenmod.weatherservice.IWeatherProviderServiceClient; +import cyanogenmod.weather.RequestInfo; + +/** @hide */ +oneway interface IWeatherProviderService { + void processWeatherUpdateRequest(in RequestInfo request); + void processCityNameLookupRequest(in RequestInfo request); + void setServiceClient(in IWeatherProviderServiceClient client); + void cancelOngoingRequests(); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weatherservice/IWeatherProviderServiceClient.aidl b/src/java/cyanogenmod/weatherservice/IWeatherProviderServiceClient.aidl new file mode 100644 index 0000000..a4baa4c --- /dev/null +++ b/src/java/cyanogenmod/weatherservice/IWeatherProviderServiceClient.aidl @@ -0,0 +1,26 @@ +/* + * 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 cyanogenmod.weatherservice; + +import cyanogenmod.weather.RequestInfo; +import cyanogenmod.weatherservice.ServiceRequestResult; + +/** @hide */ +oneway interface IWeatherProviderServiceClient { + void setServiceRequestState(in RequestInfo requestInfo, in ServiceRequestResult result, + int state); +} \ No newline at end of file diff --git a/src/java/cyanogenmod/weatherservice/ServiceRequest.java b/src/java/cyanogenmod/weatherservice/ServiceRequest.java new file mode 100644 index 0000000..e43218d --- /dev/null +++ b/src/java/cyanogenmod/weatherservice/ServiceRequest.java @@ -0,0 +1,118 @@ +/* + * 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 cyanogenmod.weatherservice; + +import android.annotation.NonNull; +import android.os.RemoteException; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.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; + + /** + * If a request is marked as cancelled, it means the client does not want to know anything about + * this request anymore + */ + private volatile boolean mCancelled; + + /* package */ ServiceRequest(RequestInfo info, IWeatherProviderServiceClient client) { + mInfo = info; + mClient = client; + } + + /** + * Obtains the request information + * @return {@link cyanogenmod.weather.RequestInfo} + */ + public RequestInfo getRequestInfo() { + return mInfo; + } + + /** + * This method should be called once the request has been completed + */ + public void complete(@NonNull ServiceRequestResult result) { + if (!mCancelled) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_LOCATION_REQ: + if (result.getWeatherInfo() == null) { + throw new IllegalStateException("The service request result does not" + + " contain a valid WeatherInfo object"); + } + mClient.setServiceRequestState(mInfo, result, + CMWeatherManager.WEATHER_REQUEST_COMPLETED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + if (result.getLocationLookupList() == null) { + //In case the user decided to mark this request as completed with an + //empty list. It's not necessarily a failure + mClient.setServiceRequestState(mInfo, null, + CMWeatherManager.LOOKUP_REQUEST_NO_MATCH_FOUND); + } else { + mClient.setServiceRequestState(mInfo, result, + CMWeatherManager.LOOKUP_REQUEST_COMPLETED); + } + break; + } + } catch (RemoteException e) { + } + } + } + + /** + * This method should be called if the service failed to process the request + * (no internet connection, time out, etc.) + */ + public void fail() { + if (!mCancelled) { + try { + final int requestType = mInfo.getRequestType(); + switch (requestType) { + case RequestInfo.TYPE_GEO_LOCATION_REQ: + case RequestInfo.TYPE_WEATHER_LOCATION_REQ: + mClient.setServiceRequestState(mInfo, null, + CMWeatherManager.WEATHER_REQUEST_FAILED); + break; + case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ: + mClient.setServiceRequestState(mInfo, null, + CMWeatherManager.LOOKUP_REQUEST_FAILED); + break; + } + } catch (RemoteException e) { + } + } + } + + /** + * 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() { + mCancelled = true; + } +} diff --git a/src/java/cyanogenmod/weatherservice/ServiceRequestResult.aidl b/src/java/cyanogenmod/weatherservice/ServiceRequestResult.aidl new file mode 100644 index 0000000..669ece5 --- /dev/null +++ b/src/java/cyanogenmod/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 cyanogenmod.weatherservice; + +parcelable ServiceRequestResult; \ No newline at end of file diff --git a/src/java/cyanogenmod/weatherservice/ServiceRequestResult.java b/src/java/cyanogenmod/weatherservice/ServiceRequestResult.java new file mode 100644 index 0000000..2e962a9 --- /dev/null +++ b/src/java/cyanogenmod/weatherservice/ServiceRequestResult.java @@ -0,0 +1,203 @@ +/* + * 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 cyanogenmod.weatherservice; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import cyanogenmod.os.Build; +import cyanogenmod.weather.WeatherLocation; +import cyanogenmod.weather.WeatherInfo; + +import java.util.ArrayList; + +/** + * Use this class to build a request result. + */ +public final class ServiceRequestResult implements Parcelable { + + private WeatherInfo mWeatherInfo; + private ArrayList mLocationLookupList; + private int mKey; + + private ServiceRequestResult() {} + + private ServiceRequestResult(Parcel in) { + int parcelableVersion = in.readInt(); + int parcelableSize = in.readInt(); + int startPosition = in.dataPosition(); + if (parcelableVersion >= Build.CM_VERSION_CODES.ELDERBERRY) { + mKey = in.readInt(); + 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--; + } + } + } + in.setDataPosition(startPosition + parcelableSize); + } + + 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) { + dest.writeInt(Build.PARCELABLE_VERSION); + + int sizePosition = dest.dataPosition(); + dest.writeInt(0); + int startPosition = dest.dataPosition(); + + // ==== ELDERBERRY ===== + dest.writeInt(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); + } + + int parcelableSize = dest.dataPosition() - startPosition; + dest.setDataPosition(sizePosition); + dest.writeInt(parcelableSize); + dest.setDataPosition(startPosition + parcelableSize); + } + + public static class Builder { + private WeatherInfo mBuilderWeatherInfo; + private ArrayList mBuilderLocationLookupList; + public Builder() { + this.mBuilderWeatherInfo = null; + this.mBuilderLocationLookupList = null; + } + + /** + * Add the supplied weather information to the result. Attempting to add a WeatherLocation + * list to the same builder will cause the system to throw IllegalArgumentException + * + * @param weatherInfo The WeatherInfo object holding the data that will be used to update + * the weather content provider + */ + public Builder setWeatherInfo(@NonNull WeatherInfo weatherInfo) { + if (mBuilderLocationLookupList != null) { + throw new IllegalArgumentException("Can't add weather information when you have" + + " already added a WeatherLocation list"); + } + + if (weatherInfo == null) { + throw new IllegalArgumentException("WeatherInfo can't be null"); + } + + mBuilderWeatherInfo = weatherInfo; + return this; + } + + /** + * Add the supplied list of WeatherLocation objects to the result. Attempting to add a + * WeatherInfo object to the same builder will cause the system to throw + * IllegalArgumentException + * + * @param locations The list of WeatherLocation objects. The list should not be null + */ + public Builder setLocationLookupList(@NonNull ArrayList locations) { + if (mBuilderWeatherInfo != null) { + throw new IllegalArgumentException("Can't add a WeatherLocation list when you have" + + " already added weather information"); + } + + mBuilderLocationLookupList = locations; + return this; + } + + /** + * Creates a {@link ServiceRequest} with the arguments + * supplied to this builder + * @return {@link ServiceRequestResult} + */ + public ServiceRequestResult build() { + ServiceRequestResult result = new ServiceRequestResult(); + result.mWeatherInfo = this.mBuilderWeatherInfo; + result.mLocationLookupList = this.mBuilderLocationLookupList; + result.mKey = this.hashCode(); + 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 ArrayList getLocationLookupList() { + return mLocationLookupList; + } + + @Override + public int hashCode() { + //The hashcode of this object was stored when it was built. This is an + //immutable object but we need to preserve the hashcode since this object is parcelable and + //it's reconstructed over IPC, and clients of this object might want to store it in a + //collection that relies on this code to identify the object + return mKey; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServiceRequestResult) { + ServiceRequestResult request = (ServiceRequestResult) obj; + return (request.hashCode() == this.mKey); + } + return false; + } +} diff --git a/src/java/cyanogenmod/weatherservice/WeatherProviderService.java b/src/java/cyanogenmod/weatherservice/WeatherProviderService.java new file mode 100644 index 0000000..759d5fc --- /dev/null +++ b/src/java/cyanogenmod/weatherservice/WeatherProviderService.java @@ -0,0 +1,196 @@ +/* + * 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 cyanogenmod.weatherservice; + +import android.annotation.SdkConstant; +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 cyanogenmod.weather.RequestInfo; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * This is the base class for implementing a weather provider service. A weather provider service + * can handle weather update requests and update the weather content provider data by processing + * a {@link ServiceRequest} + * + * A print service is declared as any other service in an AndroidManifest.xml but it must also + * specify that in handles the {@link android.content.Intent} with action + * {@link #SERVICE_INTERFACE cyanogenmod.weatherservice.WeatherProviderService}. Failure to declare + * this intent will cause the system to ignore the weather provider service. Additionally, a + * weather provider service must request the + * {@link cyanogenmod.platform.Manifest.permission#BIND_WEATHER_PROVIDER_SERVICE} permission to + * ensure that only the system can bind to it. Failure to request this permission will cause the + * system to ignore this weather provider service. Following is an example declaration: + * + *
+ *    <service android:name=".MyWeatherProviderService"
+ *          android:permission="cyanogenmod.permission.BIND_WEATHER_PROVIDER_SERVICE">
+ *      <intent-filter>
+ *          <action android:name="cyanogenmod.weatherservice.WeatherProviderService" />
+ *      <intent-filter>
+ *      . . .
+ *    </service>
+ * 
+ * + */ +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 + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE + = "cyanogenmod.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 = "cyanogenmod.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() { + mHandler.obtainMessage(ServiceHandler.MSG_CANCEL_ONGOING_REQUESTS).sendToTarget(); + } + }; + + 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_ONGOING_REQUESTS = 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_ONGOING_REQUESTS: { + synchronized (mWeakRequestsSet) { + for (final ServiceRequest request : mWeakRequestsSet) { + if (request != null) { + request.cancel(); + mWeakRequestsSet.remove(request); + mHandler.post(new Runnable() { + @Override + public void run() { + onRequestCancelled(request); + } + }); + } + } + } + } + } + } + } + + /** + * 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 diff --git a/src/java/org/cyanogenmod/internal/logging/CMMetricsLogger.java b/src/java/org/cyanogenmod/internal/logging/CMMetricsLogger.java index 26d7fe3..e3303d5 100644 --- a/src/java/org/cyanogenmod/internal/logging/CMMetricsLogger.java +++ b/src/java/org/cyanogenmod/internal/logging/CMMetricsLogger.java @@ -66,4 +66,5 @@ public class CMMetricsLogger extends MetricsLogger { public static final int TILE_HEADS_UP = BASE + 39; public static final int TILE_BATTERY_SAVER = BASE + 40; public static final int TILE_CAFFEINE = BASE + 41; + public static final int WEATHER_SETTINGS = BASE + 42; } diff --git a/system-api/cm_system-current.txt b/system-api/cm_system-current.txt index a8b7319..7365d47 100644 --- a/system-api/cm_system-current.txt +++ b/system-api/cm_system-current.txt @@ -595,6 +595,8 @@ package cyanogenmod.platform { ctor public Manifest.permission(); field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS"; field public static final java.lang.String ACCESS_THEME_MANAGER = "cyanogenmod.permission.ACCESS_THEME_MANAGER"; + field public static final java.lang.String ACCESS_WEATHER_MANAGER = "cyanogenmod.permission.ACCESS_WEATHER_MANAGER"; + field public static final java.lang.String BIND_WEATHER_PROVIDER_SERVICE = "cyanogenmod.permission.BIND_WEATHER_PROVIDER_SERVICE"; field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS"; field public static final java.lang.String LIVE_LOCK_SCREEN_MANAGER_ACCESS = "cyanogenmod.permission.LIVE_LOCK_SCREEN_MANAGER_ACCESS"; field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS"; @@ -609,11 +611,13 @@ package cyanogenmod.platform { field public static final java.lang.String READ_ALARMS = "cyanogenmod.permission.READ_ALARMS"; field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE"; field public static final java.lang.String READ_THEMES = "cyanogenmod.permission.READ_THEMES"; + field public static final java.lang.String READ_WEATHER = "cyanogenmod.permission.READ_WEATHER"; field public static final java.lang.String THIRD_PARTY_KEYGUARD = "android.permission.THIRD_PARTY_KEYGUARD"; field public static final java.lang.String WRITE_ALARMS = "cyanogenmod.permission.WRITE_ALARMS"; field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS"; field public static final java.lang.String WRITE_SETTINGS = "cyanogenmod.permission.WRITE_SETTINGS"; field public static final java.lang.String WRITE_THEMES = "cyanogenmod.permission.WRITE_THEMES"; + field public static final java.lang.String WRITE_WEATHER = "cyanogenmod.permission.WRITE_WEATHER"; } public final class R { @@ -1043,6 +1047,94 @@ package cyanogenmod.providers { field public static final int UPDATING = 2; // 0x2 } + public class WeatherContract { + ctor public WeatherContract(); + field public static final java.lang.String AUTHORITY = "com.cyanogenmod.weather"; + field public static final android.net.Uri AUTHORITY_URI; + } + + public static class WeatherContract.WeatherColumns { + ctor public WeatherContract.WeatherColumns(); + field public static final android.net.Uri CONTENT_URI; + field public static final android.net.Uri CURRENT_AND_FORECAST_WEATHER_URI; + field public static final java.lang.String CURRENT_CITY = "city"; + field public static final java.lang.String CURRENT_CITY_ID = "city_id"; + field public static final java.lang.String CURRENT_CONDITION = "condition"; + field public static final java.lang.String CURRENT_CONDITION_CODE = "condition_code"; + field public static final java.lang.String CURRENT_HUMIDITY = "humidity"; + field public static final java.lang.String CURRENT_TEMPERATURE = "temperature"; + field public static final java.lang.String CURRENT_TEMPERATURE_UNIT = "temperature_unit"; + field public static final java.lang.String CURRENT_TIMESTAMP = "timestamp"; + field public static final android.net.Uri CURRENT_WEATHER_URI; + field public static final java.lang.String CURRENT_WIND_DIRECTION = "wind_direction"; + field public static final java.lang.String CURRENT_WIND_SPEED = "wind_speed"; + field public static final java.lang.String CURRENT_WIND_SPEED_UNIT = "wind_speed_unit"; + field public static final java.lang.String FORECAST_CONDITION = "forecast_condition"; + field public static final java.lang.String FORECAST_CONDITION_CODE = "forecast_condition_code"; + field public static final java.lang.String FORECAST_HIGH = "forecast_high"; + field public static final java.lang.String FORECAST_LOW = "forecast_low"; + field public static final android.net.Uri FORECAST_WEATHER_URI; + } + + public static final class WeatherContract.WeatherColumns.TempUnit { + field public static final int CELSIUS = 1; // 0x1 + field public static final int FAHRENHEIT = 2; // 0x2 + } + + public static final class WeatherContract.WeatherColumns.WeatherCode { + field public static final int BLOWING_SNOW = 14; // 0xe + field public static final int BLUSTERY = 22; // 0x16 + field public static final int CLEAR_NIGHT = 30; // 0x1e + field public static final int CLOUDY = 25; // 0x19 + field public static final int COLD = 24; // 0x18 + field public static final int DRIZZLE = 9; // 0x9 + field public static final int DUST = 18; // 0x12 + field public static final int FAIR_DAY = 33; // 0x21 + field public static final int FAIR_NIGHT = 32; // 0x20 + field public static final int FOGGY = 19; // 0x13 + field public static final int FREEZING_DRIZZLE = 8; // 0x8 + field public static final int FREEZING_RAIN = 10; // 0xa + field public static final int HAIL = 16; // 0x10 + field public static final int HAZE = 20; // 0x14 + field public static final int HEAVY_SNOW = 39; // 0x27 + field public static final int HOT = 35; // 0x23 + field public static final int HURRICANE = 2; // 0x2 + field public static final int ISOLATED_THUNDERSHOWERS = 44; // 0x2c + field public static final int ISOLATED_THUNDERSTORMS = 36; // 0x24 + field public static final int LIGHT_SNOW_SHOWERS = 13; // 0xd + field public static final int MIXED_RAIN_AND_HAIL = 34; // 0x22 + field public static final int MIXED_RAIN_AND_SLEET = 6; // 0x6 + field public static final int MIXED_RAIN_AND_SNOW = 5; // 0x5 + field public static final int MIXED_SNOW_AND_SLEET = 7; // 0x7 + field public static final int MOSTLY_CLOUDY_DAY = 27; // 0x1b + field public static final int MOSTLY_CLOUDY_NIGHT = 26; // 0x1a + field public static final int NOT_AVAILABLE = 3200; // 0xc80 + field public static final int PARTLY_CLOUDY = 41; // 0x29 + field public static final int PARTLY_CLOUDY_DAY = 29; // 0x1d + field public static final int PARTLY_CLOUDY_NIGHT = 28; // 0x1c + field public static final int SCATTERED_SHOWERS = 38; // 0x26 + field public static final int SCATTERED_SNOW_SHOWERS = 40; // 0x28 + field public static final int SCATTERED_THUNDERSTORMS = 37; // 0x25 + field public static final int SEVERE_THUNDERSTORMS = 3; // 0x3 + field public static final int SHOWERS = 11; // 0xb + field public static final int SLEET = 17; // 0x11 + field public static final int SMOKY = 21; // 0x15 + field public static final int SNOW = 15; // 0xf + field public static final int SNOW_FLURRIES = 12; // 0xc + field public static final int SNOW_SHOWERS = 43; // 0x2b + field public static final int SUNNY = 31; // 0x1f + field public static final int THUNDERSHOWER = 42; // 0x2a + field public static final int THUNDERSTORMS = 4; // 0x4 + field public static final int TORNADO = 0; // 0x0 + field public static final int TROPICAL_STORM = 1; // 0x1 + field public static final int WINDY = 23; // 0x17 + } + + public static final class WeatherContract.WeatherColumns.WindSpeedUnit { + field public static final int KPH = 1; // 0x1 + field public static final int MPH = 2; // 0x2 + } + } package cyanogenmod.themes { @@ -1155,3 +1247,147 @@ package cyanogenmod.util { } +package cyanogenmod.weather { + + public class CMWeatherManager { + method public java.lang.String getActiveWeatherServiceProviderLabel(); + method public static cyanogenmod.weather.CMWeatherManager getInstance(android.content.Context); + method public void lookupCity(java.lang.String, cyanogenmod.weather.CMWeatherManager.LookupCityRequestListener); + method public void registerWeatherServiceProviderChangeListener(cyanogenmod.weather.CMWeatherManager.WeatherServiceProviderChangeListener); + method public void requestWeatherUpdate(android.location.Location, cyanogenmod.weather.CMWeatherManager.WeatherUpdateRequestListener); + method public void requestWeatherUpdate(cyanogenmod.weather.WeatherLocation, cyanogenmod.weather.CMWeatherManager.WeatherUpdateRequestListener); + method public void unregisterWeatherServiceProviderChangeListener(cyanogenmod.weather.CMWeatherManager.WeatherServiceProviderChangeListener); + field public static final int WEATHER_REQUEST_ALREADY_IN_PROGRESS = -3; // 0xfffffffd + field public static final int WEATHER_REQUEST_COMPLETED = 1; // 0x1 + field public static final int WEATHER_REQUEST_FAILED = -2; // 0xfffffffe + field public static final int WEATHER_REQUEST_SUBMITTED_TOO_SOON = -1; // 0xffffffff + } + + public static abstract interface CMWeatherManager.LookupCityRequestListener { + method public abstract void onLookupCityRequestCompleted(java.util.ArrayList); + } + + public static abstract interface CMWeatherManager.WeatherServiceProviderChangeListener { + method public abstract void onWeatherServiceProviderChanged(java.lang.String); + } + + public static abstract interface CMWeatherManager.WeatherUpdateRequestListener { + method public abstract void onWeatherRequestCompleted(int, cyanogenmod.weather.WeatherInfo); + } + + public final class RequestInfo implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCityName(); + method public android.location.Location getLocation(); + method public int getRequestType(); + method public int getTemperatureUnit(); + method public cyanogenmod.weather.WeatherLocation getWeatherLocation(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int TYPE_GEO_LOCATION_REQ = 1; // 0x1 + field public static final int TYPE_LOOKUP_CITY_NAME_REQ = 3; // 0x3 + field public static final int TYPE_WEATHER_LOCATION_REQ = 2; // 0x2 + } + + public final class WeatherInfo implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCity(); + method public java.lang.String getCityId(); + method public int getConditionCode(); + method public java.util.ArrayList getForecasts(); + method public float getHumidity(); + method public float getTemperature(); + method public int getTemperatureUnit(); + method public long getTimestamp(); + method public float getWindDirection(); + method public float getWindSpeed(); + method public int getWindSpeedUnit(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherInfo.Builder { + ctor public WeatherInfo.Builder(long); + method public cyanogenmod.weather.WeatherInfo build(); + method public cyanogenmod.weather.WeatherInfo.Builder setCity(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherInfo.Builder setForecast(java.util.ArrayList); + method public cyanogenmod.weather.WeatherInfo.Builder setHumidity(float); + method public cyanogenmod.weather.WeatherInfo.Builder setTemperature(float, int); + method public cyanogenmod.weather.WeatherInfo.Builder setWeatherCondition(int); + method public cyanogenmod.weather.WeatherInfo.Builder setWind(float, float, int); + } + + public static class WeatherInfo.DayForecast implements android.os.Parcelable { + method public int describeContents(); + method public int getConditionCode(); + method public float getHigh(); + method public float getLow(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherInfo.DayForecast.Builder { + ctor public WeatherInfo.DayForecast.Builder(); + method public cyanogenmod.weather.WeatherInfo.DayForecast build(); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setHigh(float); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setLow(float); + method public cyanogenmod.weather.WeatherInfo.DayForecast.Builder setWeatherCondition(int); + } + + public final class WeatherLocation implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getCity(); + method public java.lang.String getCityId(); + method public java.lang.String getCountry(); + method public java.lang.String getCountryId(); + method public java.lang.String getPostalCode(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class WeatherLocation.Builder { + ctor public WeatherLocation.Builder(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherLocation build(); + method public cyanogenmod.weather.WeatherLocation.Builder setCountry(java.lang.String, java.lang.String); + method public cyanogenmod.weather.WeatherLocation.Builder setPostalCode(java.lang.String); + } + +} + +package cyanogenmod.weatherservice { + + public final class ServiceRequest { + method public void complete(cyanogenmod.weatherservice.ServiceRequestResult); + method public void fail(); + method public cyanogenmod.weather.RequestInfo getRequestInfo(); + } + + public final class ServiceRequestResult implements android.os.Parcelable { + method public int describeContents(); + method public java.util.ArrayList getLocationLookupList(); + method public cyanogenmod.weather.WeatherInfo getWeatherInfo(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static class ServiceRequestResult.Builder { + ctor public ServiceRequestResult.Builder(); + method public cyanogenmod.weatherservice.ServiceRequestResult build(); + method public cyanogenmod.weatherservice.ServiceRequestResult.Builder setLocationLookupList(java.util.ArrayList); + method public cyanogenmod.weatherservice.ServiceRequestResult.Builder setWeatherInfo(cyanogenmod.weather.WeatherInfo); + } + + public abstract class WeatherProviderService extends android.app.Service { + ctor public WeatherProviderService(); + method protected final void attachBaseContext(android.content.Context); + method public final android.os.IBinder onBind(android.content.Intent); + method protected void onConnected(); + method protected void onDisconnected(); + method protected abstract void onRequestCancelled(cyanogenmod.weatherservice.ServiceRequest); + method protected abstract void onRequestSubmitted(cyanogenmod.weatherservice.ServiceRequest); + field public static final java.lang.String SERVICE_INTERFACE = "cyanogenmod.weatherservice.WeatherProviderService"; + field public static final java.lang.String SERVICE_META_DATA = "cyanogenmod.weatherservice"; + } + +} +