diff options
author | Luis Vidal <lvidal@cyngn.com> | 2016-03-21 11:56:40 -0700 |
---|---|---|
committer | Luis Vidal <lvidal@cyngn.com> | 2016-03-31 17:36:53 -0700 |
commit | 4195a1cf89d8c7bea024b6922bc2145ee05015f3 (patch) | |
tree | 9fc4228233537327cf54fe7c9c4820b57909e9d7 /cm | |
parent | 46821304e984bbf2ac3552ebb6f0a55440cf35e5 (diff) | |
download | vendor_cmsdk-4195a1cf89d8c7bea024b6922bc2145ee05015f3.zip vendor_cmsdk-4195a1cf89d8c7bea024b6922bc2145ee05015f3.tar.gz vendor_cmsdk-4195a1cf89d8c7bea024b6922bc2145ee05015f3.tar.bz2 |
Add Weather Content Provider [4/5]
Introduce CM Weather Manager and Weather Provider Services API.
The CM Weather Manager can be used by apps to request weather
updates.
The Weather Provider Services API allows a third party developer to
implement a weather service to process weather update requests and
update the CM Weather Content Provider data which can be consumed by
any other app holding the required permission.
Change-Id: Idcc80712ba92715109d3577d120f7fea85d6c996
Diffstat (limited to 'cm')
-rw-r--r-- | cm/lib/main/java/org/cyanogenmod/platform/internal/CMWeatherManagerService.java | 545 | ||||
-rw-r--r-- | cm/res/AndroidManifest.xml | 27 | ||||
-rw-r--r-- | cm/res/res/values/config.xml | 1 | ||||
-rw-r--r-- | cm/res/res/values/strings.xml | 10 |
4 files changed, 582 insertions, 1 deletions
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<IWeatherServiceProviderChangeListener> 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<ContentValues> 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" /> + <!-- Allows an application to read the weather content from the provider--> + <permission android:name="cyanogenmod.permission.READ_WEATHER" + android:label="@string/permlab_weather_read" + android:description="@string/permdesc_weather_read" + android:protectionLevel="normal"/> + + <!-- Allows an application to update the content of the weather provider + <p>Not for use by third-party applications. --> + <permission android:name="cyanogenmod.permission.WRITE_WEATHER" + android:label="@string/permlab_weather_write" + android:description="@string/permdesc_weather_write" + android:protectionLevel="signature|privileged" /> + + <!-- Allows an application to be identified as a weather provider service --> + <permission android:name="cyanogenmod.permission.BIND_WEATHER_PROVIDER_SERVICE" + android:label="@string/permlab_weather_bind" + android:description="@string/permdesc_weather_bind" + android:protectionLevel="signature"/> + + <!-- Allows an application to access the weather service. + <p>Although the protection is normal, this permission should be required ONLY by those apps + meant to do something meaningful with the data provided by the service (LockClock, SysUI)--> + <permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" + android:label="@string/permlab_weather_access_mgr" + android:description="@string/permdesc_weather_access_mgr" + android:protectionLevel="normal"/> + <application android:process="system" android:persistent="true" android:hasCode="false" diff --git a/cm/res/res/values/config.xml b/cm/res/res/values/config.xml index 179ade2..3a5b7d2 100644 --- a/cm/res/res/values/config.xml +++ b/cm/res/res/values/config.xml @@ -90,5 +90,6 @@ <item>org.cyanogenmod.platform.internal.ThemeManagerService</item> <item>org.cyanogenmod.platform.internal.IconCacheManagerService</item> <item>org.cyanogenmod.platform.internal.LiveLockScreenServiceBroker</item> + <item>org.cyanogenmod.platform.internal.CMWeatherManagerService</item> </string-array> </resources> 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 @@ <!-- Live lock screen manager service provider permission description --> <string name="permdesc_accessLiveLockScreenServiceProvider">Allows a service to provide the live lock screen manager service.</string> - + <!-- Weather Service strings --> + <string name="permlab_weather_read">read weather</string> + <string name="permdesc_weather_read">Allows an app to read content from the weather provider</string> + <string name="permlab_weather_write">update weather provider</string> + <string name="permdesc_weather_write">Allows an app to update the content of the weather provider</string> + <string name="permlab_weather_bind">binds as a weather provider service</string> + <string name="permdesc_weather_bind">Allows an app to be identified as a weather provider service</string> + <string name="permlab_weather_access_mgr">access weather service</string> + <string name="permdesc_weather_access_mgr">Allows an app to access the weather service in the system. Should never be needed for normal apps</string> </resources> |