Commit afe47bcd authored by Luis Vidal's avatar Luis Vidal
Browse files

Refactor OpenWeatherMap weather provider service

An attempt to improve the legacy code that was originally
ported from LockClock

Change-Id: If04ecaceef66c7c9af14a1710f5d66ea314b3f7c
parent f471c4c3
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
out/
build/
# Local configuration file (sdk path, etc)
local.properties
# Eclipse project files
.classpath
.project
# Windows thumbnail db
.DS_Store
# IDEA/Android Studio project files, because
# the project can be imported from settings.gradle
.idea
*.iml
# Old-style IDEA project files
*.ipr
*.iws
# Local IDEA workspace
.idea/workspace.xml
# Gradle cache
.gradle
# Sandbox stuff
_sandbox
#
# 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := OpenWeatherMapProvider
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := \
org.cyanogenmod.platform.sdk
include $(BUILD_PACKAGE)
\ No newline at end of file
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
applicationId "org.cyanogenmod.openweathermapprovider"
minSdkVersion 23
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
}
dependencies {
compile 'org.cyanogenmod:platform.sdk:5.+'
compile 'com.google.code.gson:gson:2.6.1'
compile 'com.squareup.retrofit2:retrofit:2.0.1'
compile 'com.squareup.retrofit2:converter-gson:2.0.1'
compile 'com.squareup.okhttp3:okhttp:3.2.0'
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/lvidal/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2012 The CyanogenMod Project (DvTonder)
Copyright (C) 2016 The CyanogenMod Project
<!-- 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.
......@@ -15,28 +14,35 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.cyanogenmod.openweathermapprovider" >
package="org.cyanogenmod.openweathermapprovider">
<uses-permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="org.cyanogenmod.weather" android:required="true" />
<application
android:label="@string/app_name" android:allowBackup="true">
<service
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".OpenWeatherMapProviderService"
android:label="@string/app_name"
android:exported="true"
android:permission="cyanogenmod.permission.BIND_WEATHER_PROVIDER_SERVICE">
<intent-filter>
<action android:name="cyanogenmod.weatherservice.WeatherProviderService" />
</intent-filter>
<meta-data
<intent-filter>
<action android:name="cyanogenmod.weatherservice.WeatherProviderService" />
</intent-filter>
<meta-data
android:name="cyanogenmod.weatherservice"
android:resource="@xml/openweathermap" />
</service>
<activity android:name=".SettingsActivity"
android:label="@string/app_name"
android:exported="true" />
</application>
</service>
<activity android:name=".SettingsActivity"
android:label="@string/app_name"
android:exported="true" />
</application>
</manifest>
/*
* 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.openweathermapprovider;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.AsyncTask;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import org.cyanogenmod.openweathermapprovider.openweathermap.OpenWeatherMapService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cyanogenmod.weather.CMWeatherManager;
import cyanogenmod.weather.RequestInfo;
import cyanogenmod.weather.WeatherInfo;
import cyanogenmod.weather.WeatherLocation;
import cyanogenmod.weatherservice.ServiceRequest;
import cyanogenmod.weatherservice.ServiceRequestResult;
import cyanogenmod.weatherservice.WeatherProviderService;
import static org.cyanogenmod.openweathermapprovider.utils.Logging.logd;
import static org.cyanogenmod.openweathermapprovider.utils.Logging.logw;
public class OpenWeatherMapProviderService extends WeatherProviderService
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String API_KEY = "api_key";
private static final String API_KEY_VERIFIED_STATE = "api_key_verified_state";
private static final int API_KEY_INVALID = 0;
private static final int API_KEY_VERIFIED = 2;
private OpenWeatherMapService mOpenWeatherMapService;
private Map<ServiceRequest,WeatherUpdateRequestTask> mWeatherUpdateRequestMap = new HashMap<>();
private Map<ServiceRequest,LookupCityNameRequestTask> mLookupCityRequestMap = new HashMap<>();
//OpenWeatherMap recommends to wait 10 min between requests
private final static long REQUEST_THRESHOLD = 1000L * 60L * 10L;
private long mLastRequestTimestamp = -REQUEST_THRESHOLD;
private WeatherLocation mLastWeatherLocation;
private Location mLastLocation;
//5km of threshold, the weather won't change that much in such short distance
private static final float LOCATION_DISTANCE_METERS_THRESHOLD = 5f * 1000f;
@Override
public void onCreate() {
mOpenWeatherMapService = new OpenWeatherMapService(this);
}
@Override
public void onConnected() {
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(this);
preferences.registerOnSharedPreferenceChangeListener(this);
final String mApiId = preferences.getString(API_KEY, null);
mOpenWeatherMapService.setApiKey(mApiId);
}
@Override
public void onDisconnected() {
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(this);
preferences.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
protected void onRequestSubmitted(ServiceRequest request) {
RequestInfo requestInfo = request.getRequestInfo();
int requestType = requestInfo.getRequestType();
logd("Received request type " + requestType);
if (((requestType == RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ &&
isSameGeoLocation(requestInfo.getLocation(), mLastLocation))
|| (requestType == RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ &&
isSameWeatherLocation(requestInfo.getWeatherLocation(),
mLastWeatherLocation))) && wasRequestSubmittedTooSoon()) {
request.reject(CMWeatherManager.RequestStatus.SUBMITTED_TOO_SOON);
return;
}
switch (requestType) {
case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ:
case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
synchronized (mWeatherUpdateRequestMap) {
WeatherUpdateRequestTask updateTask
= new WeatherUpdateRequestTask(request);
mWeatherUpdateRequestMap.put(request, updateTask);
mLastRequestTimestamp = SystemClock.elapsedRealtime();
updateTask.execute();
}
break;
case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ:
synchronized (mLookupCityRequestMap) {
LookupCityNameRequestTask lookupTask = new LookupCityNameRequestTask(request);
mLookupCityRequestMap.put(request, lookupTask);
lookupTask.execute();
}
break;
}
}
@Override
protected void onRequestCancelled(ServiceRequest request) {
switch (request.getRequestInfo().getRequestType()) {
case RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ:
case RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ:
synchronized (mWeatherUpdateRequestMap) {
WeatherUpdateRequestTask task = mWeatherUpdateRequestMap.remove(request);
if (task != null) {
task.cancel(true);
}
return;
}
case RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ:
synchronized (mLookupCityRequestMap) {
LookupCityNameRequestTask task = mLookupCityRequestMap.remove(request);
if (task != null) {
task.cancel(true);
}
}
return;
default:
logw("Received unknown request type "
+ request.getRequestInfo().getRequestType());
break;
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(API_KEY)) {
logd("API key has changed");
final String mApiKey = sharedPreferences.getString(key, null);
mOpenWeatherMapService.setApiKey(mApiKey);
}
}
private boolean isSameWeatherLocation(WeatherLocation newLocation,
WeatherLocation oldLocation) {
if (newLocation == null || oldLocation == null) return false;
return (newLocation.getCityId().equals(oldLocation.getCityId())
&& newLocation.getCity().equals(oldLocation.getCity())
&& newLocation.getPostalCode().equals(oldLocation.getPostalCode())
&& newLocation.getCountry().equals(oldLocation.getCountry())
&& newLocation.getCountryId().equals(oldLocation.getCountryId()));
}
private boolean isSameGeoLocation(Location newLocation, Location oldLocation) {
if (newLocation == null || oldLocation == null) return false;
float distance = newLocation.distanceTo(oldLocation);
logd("Distance between locations " + distance);
return (distance < LOCATION_DISTANCE_METERS_THRESHOLD);
}
private boolean wasRequestSubmittedTooSoon() {
final long now = SystemClock.elapsedRealtime();
logd("Now " + now + " last request " + mLastRequestTimestamp);
return (mLastRequestTimestamp + REQUEST_THRESHOLD > now);
}
private class WeatherUpdateRequestTask extends AsyncTask<Void, Void, WeatherInfo> {
final private ServiceRequest mRequest;
public WeatherUpdateRequestTask(ServiceRequest request) {
mRequest = request;
}
@Override
protected WeatherInfo doInBackground(Void... params) {
RequestInfo requestInfo = mRequest.getRequestInfo();
int requestType = requestInfo.getRequestType();
if (requestType == RequestInfo.TYPE_WEATHER_BY_WEATHER_LOCATION_REQ) {
try {
return mOpenWeatherMapService.queryWeather(requestInfo.getWeatherLocation());
} catch (OpenWeatherMapService.InvalidApiKeyException e) {
setApiKeyVerified(API_KEY_INVALID);
return null;
}
} else if (requestType == RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ) {
try {
return mOpenWeatherMapService.queryWeather(requestInfo.getLocation());
} catch (OpenWeatherMapService.InvalidApiKeyException e) {
setApiKeyVerified(API_KEY_INVALID);
return null;
}
} else {
// We don't know how to handle any other type of request
logw("Received unknown request type "+ requestType);
return null;
}
}
@Override
protected void onPostExecute(WeatherInfo weatherInfo) {
if (weatherInfo == null) {
logd("Received null weather info, failing request");
mRequest.fail();
} else {
logd(weatherInfo.toString());
ServiceRequestResult result = new ServiceRequestResult.Builder(weatherInfo).build();
mRequest.complete(result);
if (mRequest.getRequestInfo().getRequestType()
== RequestInfo.TYPE_WEATHER_BY_GEO_LOCATION_REQ) {
mLastLocation = mRequest.getRequestInfo().getLocation();
} else {
mLastWeatherLocation = mRequest.getRequestInfo().getWeatherLocation();
}
setApiKeyVerified(API_KEY_VERIFIED);
}
synchronized (mWeatherUpdateRequestMap) {
mWeatherUpdateRequestMap.remove(mRequest);
}
}
}
private class LookupCityNameRequestTask extends AsyncTask<Void, Void, List<WeatherLocation>> {
final private ServiceRequest mRequest;
public LookupCityNameRequestTask(ServiceRequest request) {
mRequest = request;
}
@Override
protected List<WeatherLocation> doInBackground(Void... params) {
RequestInfo requestInfo = mRequest.getRequestInfo();
if (requestInfo.getRequestType() != RequestInfo.TYPE_LOOKUP_CITY_NAME_REQ) {
logw("Received unsupported request type " + requestInfo.getRequestType());
return null;
}
try {
return mOpenWeatherMapService.lookupCity(mRequest.getRequestInfo().getCityName());
} catch (OpenWeatherMapService.InvalidApiKeyException e) {
setApiKeyVerified(API_KEY_INVALID);
return null;
}
}
@Override
protected void onPostExecute(List<WeatherLocation> locations) {
if (locations != null) {
for (WeatherLocation location : locations) {
logd(location.toString());
}
ServiceRequestResult request = new ServiceRequestResult.Builder(locations).build();
mRequest.complete(request);
setApiKeyVerified(API_KEY_VERIFIED);
} else {
mRequest.fail();
}
synchronized (mLookupCityRequestMap) {
mLookupCityRequestMap.remove(mRequest);
}
}
}
private void setApiKeyVerified(int state) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().putInt(API_KEY_VERIFIED_STATE, state).apply();
}
}
......@@ -18,6 +18,7 @@ package org.cyanogenmod.openweathermapprovider;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
......@@ -27,32 +28,43 @@ import android.widget.Toast;
public class SettingsActivity extends Activity {
private final static String API_KEY = "api_key";
private static final String API_KEY = "api_key";
private static final String API_KEY_VERIFIED_STATE = "api_key_verified_state";
private static final int API_KEY_PENDING_VERIFICATION = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new ServicePrefsFragment())
.commit();
}
public static class ServicePrefsFragment extends PreferenceFragment {
public static class ServicePrefsFragment extends PreferenceFragment implements Preference.OnPreferenceChangeListener {
private EditTextPreference mApiKeyPreference;
@Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
addPreferencesFromResource(R.xml.preferences);
//Format some strings with arguments
EditTextPreference apiKey = (EditTextPreference)findPreference("api_key");
apiKey.setSummary(getString(R.string.prefscreen_api_key_summary,
getString(R.string.app_name)));
Preference copyright = findPreference("copyright");
copyright.setSummary(getString(R.string.prefscreen_copyright_summary,
getString(R.string.openweathermap_inc_name)));
mApiKeyPreference = (EditTextPreference) findPreference(API_KEY);
SharedPreferences sharedPreferences
= PreferenceManager.getDefaultSharedPreferences(getActivity());
int apiKeyVerificationState = sharedPreferences.getInt(API_KEY_VERIFIED_STATE, -1);
try {
//lookup the value state
String[] stateEntries
= getResources().getStringArray(R.array.api_key_states_entries);
String state = stateEntries[apiKeyVerificationState];
mApiKeyPreference.setSummary(state);
} catch (IndexOutOfBoundsException e) {
mApiKeyPreference.setSummary(getString(R.string.prefscreen_api_key_summary,
getString(R.string.app_name)));
}
}
@Override
......@@ -60,13 +72,31 @@ public class SettingsActivity extends Activity {
super.onResume();
Context context = getActivity();
if (context != null) {
String apiKey = getPreferenceManager().getSharedPreferences()
.getString(API_KEY, null);
SharedPreferences sp = getPreferenceManager().getSharedPreferences();
String apiKey = sp.getString(API_KEY, null);
if (apiKey == null || apiKey.equals("")) {
Toast.makeText(context, getString(R.string.api_key_not_set_message,
getString(R.string.app_name)), Toast.LENGTH_LONG).show();
}
mApiKeyPreference.setOnPreferenceChangeListener(this);
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
switch (preference.getKey()) {
case API_KEY:
SharedPreferences sharedPreferences
= PreferenceManager.getDefaultSharedPreferences(getActivity());
sharedPreferences.edit().putInt(API_KEY_VERIFIED_STATE,
API_KEY_PENDING_VERIFICATION).apply();
mApiKeyPreference.setSummary(getResources().getStringArray(
R.array.api_key_states_entries)[API_KEY_PENDING_VERIFICATION]);
Toast.makeText(getActivity(), R.string.api_key_changed_verification_warning,
Toast.LENGTH_LONG).show();
return true;
}
return false;
}
}
}
/*
* 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.openweathermapprovider.openweathermap;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
import cyanogenmod.providers.WeatherContract;