diff options
author | Danny Baumann <dannybaumann@web.de> | 2014-01-05 14:13:16 +0100 |
---|---|---|
committer | Danny Baumann <dannybaumann@web.de> | 2014-01-07 12:06:59 +0100 |
commit | f1487b06a4671ddc79f713aaac4f9a425a3937e1 (patch) | |
tree | 9445aadafaccf7a6f5ec9593238b8c6f323197ec /src | |
parent | c547e3dce2216eb2879a03f07266d3845da11c01 (diff) | |
download | android_packages_apps_LockClock-f1487b06a4671ddc79f713aaac4f9a425a3937e1.tar.gz android_packages_apps_LockClock-f1487b06a4671ddc79f713aaac4f9a425a3937e1.tar.bz2 android_packages_apps_LockClock-f1487b06a4671ddc79f713aaac4f9a425a3937e1.zip |
Port over improvements from Chronus
- new weather source: OpenWeatherMap
- weather icon pack support
- weather forecast activity and popup
- updated weather and in-app icons
- some new translations (AR, TR, SL)
Change-Id: I2bcc2042bf83d0e0bb4a00200de1310042303e9c
Diffstat (limited to 'src')
19 files changed, 1567 insertions, 196 deletions
diff --git a/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java b/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java index d2265cb..0441e00 100644 --- a/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java +++ b/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java @@ -25,6 +25,7 @@ import android.util.Log; import com.cyanogenmod.lockclock.misc.Constants; import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.weather.ForecastActivity; import com.cyanogenmod.lockclock.weather.WeatherUpdateService; import com.cyanogenmod.lockclock.ClockWidgetService; import com.cyanogenmod.lockclock.WidgetApplication; @@ -65,7 +66,7 @@ public class ClockWidgetProvider extends AppWidgetProvider { } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { // On first boot lastUpdate will be 0 thus no need to force an update // Subsequent boots will use cached data - WeatherUpdateService.scheduleNextUpdate(context); + WeatherUpdateService.scheduleNextUpdate(context, false); // A widget has been deleted, prevent our handling and ask the super class handle it } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action) @@ -86,6 +87,12 @@ public class ClockWidgetProvider extends AppWidgetProvider { } else if (ClockWidgetService.ACTION_HIDE_CALENDAR.equals(action)) { updateWidgets(context, false, true); + // The intent is to launch the modal pop-up forecast dialog + } else if (Constants.ACTION_SHOW_FORECAST.equals(action)) { + Intent i = new Intent(context, ForecastActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + // Something we did not handle, let the super class deal with it. // This includes the REFRESH_CLOCK intent from Clock settings } else { @@ -117,7 +124,7 @@ public class ClockWidgetProvider extends AppWidgetProvider { @Override public void onEnabled(Context context) { if (D) Log.d(TAG, "Scheduling next weather update"); - WeatherUpdateService.scheduleNextUpdate(context); + WeatherUpdateService.scheduleNextUpdate(context, true); // Start the broadcast receiver (API 16 devices) // This will schedule a repeating alarm every minute to handle the clock refresh diff --git a/src/com/cyanogenmod/lockclock/ClockWidgetService.java b/src/com/cyanogenmod/lockclock/ClockWidgetService.java index 1698122..ea5da49 100755 --- a/src/com/cyanogenmod/lockclock/ClockWidgetService.java +++ b/src/com/cyanogenmod/lockclock/ClockWidgetService.java @@ -35,8 +35,9 @@ import android.util.TypedValue; import android.view.View; import android.widget.RemoteViews; -import com.cyanogenmod.lockclock.calendar.CalendarWidgetService; +import com.cyanogenmod.lockclock.calendar.CalendarViewsService; import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.IconUtils; import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.misc.WidgetUtils; import com.cyanogenmod.lockclock.weather.WeatherInfo; @@ -328,10 +329,11 @@ public class ClockWidgetService extends IntentService { if (!TextUtils.isEmpty(nextAlarm)) { // An alarm is set, deal with displaying it int color = Preferences.clockAlarmFontColor(this); + final Resources res = getResources(); // Overlay the selected color on the alarm icon and set the imageview alarmViews.setImageViewBitmap(R.id.alarm_icon, - WidgetUtils.getOverlaidBitmap(this, R.drawable.ic_alarm_small, color)); + IconUtils.getOverlaidBitmap(res, R.drawable.ic_alarm_small, color)); alarmViews.setViewVisibility(R.id.alarm_icon, View.VISIBLE); if (!smallWidget) { @@ -389,19 +391,18 @@ public class ClockWidgetService extends IntentService { private void setWeatherData(RemoteViews weatherViews, boolean smallWidget, WeatherInfo w) { int color = Preferences.weatherFontColor(this); int timestampColor = Preferences.weatherTimestampFontColor(this); - boolean colorIcons = Preferences.useAlternateWeatherIcons(this); + String iconsSet = Preferences.getWeatherIconSet(this); // Reset no weather visibility weatherViews.setViewVisibility(R.id.weather_no_data, View.GONE); weatherViews.setViewVisibility(R.id.weather_refresh, View.GONE); // Weather Image - if (colorIcons) { - // No additional color overlays needed - weatherViews.setImageViewResource(R.id.weather_image, w.getConditionResource()); + int resId = w.getConditionResource(iconsSet); + if (resId != 0) { + weatherViews.setImageViewResource(R.id.weather_image, w.getConditionResource(iconsSet)); } else { - // Overlay the condition image with the appropriate color - weatherViews.setImageViewBitmap(R.id.weather_image, w.getConditionBitmap(color)); + weatherViews.setImageViewBitmap(R.id.weather_image, w.getConditionBitmap(iconsSet, color)); } // Weather Condition @@ -448,7 +449,7 @@ public class ClockWidgetService extends IntentService { } // Register an onClickListener on Weather - setWeatherClickListener(weatherViews); + setWeatherClickListener(weatherViews, false); } /** @@ -459,7 +460,8 @@ public class ClockWidgetService extends IntentService { boolean firstRun = Preferences.isFirstWeatherUpdate(this); // Hide the normal weather stuff - String noData = getString(R.string.weather_cannot_reach_provider, getString(R.string.weather_source)); + int providerNameResource = Preferences.weatherProvider(this).getNameResourceId(); + String noData = getString(R.string.weather_cannot_reach_provider, getString(providerNameResource)); weatherViews.setViewVisibility(R.id.weather_image, View.INVISIBLE); if (!smallWidget) { weatherViews.setViewVisibility(R.id.weather_city, View.GONE); @@ -485,32 +487,44 @@ public class ClockWidgetService extends IntentService { // Register an onClickListener on Weather with the default (Refresh) action if (!firstRun) { - setWeatherClickListener(weatherViews); + setWeatherClickListener(weatherViews, true); } } - private void setWeatherClickListener(RemoteViews weatherViews) { - weatherViews.setOnClickPendingIntent(R.id.weather_panel, - WeatherUpdateService.getUpdateIntent(this, true)); + private void setWeatherClickListener(RemoteViews weatherViews, boolean forceRefresh) { + // Register an onClickListener on the Weather panel, default action is show forecast + PendingIntent pi = null; + if (forceRefresh) { + pi = WeatherUpdateService.getUpdateIntent(this, true); + } + + if (pi == null) { + Intent i = new Intent(this, ClockWidgetProvider.class); + i.setAction(Constants.ACTION_SHOW_FORECAST); + pi = PendingIntent.getBroadcast(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); + } + weatherViews.setOnClickPendingIntent(R.id.weather_panel, pi); } + //=============================================================================================== // Calendar related functionality //=============================================================================================== private void refreshCalendar(RemoteViews calendarViews, int widgetId) { + final Resources res = getResources(); // Calendar icon: Overlay the selected color and set the imageview int color = Preferences.calendarFontColor(this); // Hide the icon if preference set if (Preferences.showCalendarIcon(this)) { calendarViews.setImageViewBitmap(R.id.calendar_icon, - WidgetUtils.getOverlaidBitmap(this, R.drawable.ic_lock_idle_calendar, color)); + IconUtils.getOverlaidBitmap(res, R.drawable.ic_lock_idle_calendar, color)); } else { calendarViews.setImageViewBitmap(R.id.calendar_icon, null); } // Set up and start the Calendar RemoteViews service - final Intent remoteAdapterIntent = new Intent(this, CalendarWidgetService.class); + final Intent remoteAdapterIntent = new Intent(this, CalendarViewsService.class); remoteAdapterIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); remoteAdapterIntent.setData(Uri.parse(remoteAdapterIntent.toUri(Intent.URI_INTENT_SCHEME))); calendarViews.setRemoteAdapter(R.id.calendar_list, remoteAdapterIntent); diff --git a/src/com/cyanogenmod/lockclock/calendar/CalendarWidgetService.java b/src/com/cyanogenmod/lockclock/calendar/CalendarViewsService.java index a073280..893d8f7 100755 --- a/src/com/cyanogenmod/lockclock/calendar/CalendarWidgetService.java +++ b/src/com/cyanogenmod/lockclock/calendar/CalendarViewsService.java @@ -48,7 +48,7 @@ import java.util.Calendar; import java.util.Date; import java.util.Set; -public class CalendarWidgetService extends RemoteViewsService { +public class CalendarViewsService extends RemoteViewsService { @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { diff --git a/src/com/cyanogenmod/lockclock/misc/Constants.java b/src/com/cyanogenmod/lockclock/misc/Constants.java index db1b89f..751de2d 100755 --- a/src/com/cyanogenmod/lockclock/misc/Constants.java +++ b/src/com/cyanogenmod/lockclock/misc/Constants.java @@ -34,6 +34,7 @@ public class Constants { public static final String CLOCK_AM_PM_INDICATOR = "clock_am_pm_indicator"; public static final String SHOW_WEATHER = "show_weather"; + public static final String WEATHER_SOURCE = "weather_source"; public static final String WEATHER_USE_CUSTOM_LOCATION = "weather_use_custom_location"; public static final String WEATHER_CUSTOM_LOCATION_ID = "weather_custom_location_id"; public static final String WEATHER_CUSTOM_LOCATION_CITY = "weather_custom_location_city"; @@ -42,12 +43,13 @@ public class Constants { public static final String WEATHER_USE_METRIC = "weather_use_metric"; public static final String WEATHER_INVERT_LOWHIGH = "weather_invert_lowhigh"; public static final String WEATHER_REFRESH_INTERVAL = "weather_refresh_interval"; - public static final String WEATHER_USE_ALTERNATE_ICONS = "weather_use_alternate_icons"; public static final String WEATHER_LOCATION_ID = "weather_woeid"; public static final String WEATHER_SHOW_WHEN_MINIMIZED = "weather_show_when_minimized"; public static final String WEATHER_FONT_COLOR = "weather_font_color"; public static final String WEATHER_TIMESTAMP_FONT_COLOR = "weather_timestamp_font_color"; - + public static final String WEATHER_ICONS = "weather_icons"; + public static final String MONOCHROME = "mono"; + public static final String COLOR_STD = "color"; public static final String SHOW_CALENDAR = "show_calendar"; public static final String CALENDAR_LIST = "calendar_list"; public static final String CALENDAR_LOOKAHEAD = "calendar_lookahead"; @@ -89,5 +91,8 @@ public class Constants { public static final String DEFAULT_LIGHT_COLOR = "#ffffffff"; public static final String DEFAULT_DARK_COLOR = "#80ffffff"; + + // Intent actions + public static final String ACTION_SHOW_FORECAST = "com.cyanogenmod.lockclock.action.SHOW_FORECAST"; } diff --git a/src/com/cyanogenmod/lockclock/misc/IconUtils.java b/src/com/cyanogenmod/lockclock/misc/IconUtils.java new file mode 100644 index 0000000..437d075 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/misc/IconUtils.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2013 The CyanogenMod Project (DvTonder) + * + * 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 com.cyanogenmod.lockclock.misc; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.Log; +import com.cyanogenmod.lockclock.R; + +public class IconUtils { + private static final String TAG = "IconUtils"; + private static boolean D = Constants.DEBUG; + + public static int getWeatherIconResource(Context context, String iconSet, int conditionCode) { + if (iconSet.startsWith("ext:") || iconSet.equals(Constants.MONOCHROME)) { + return 0; + } + + final Resources res = context.getResources(); + final int resId = res.getIdentifier("weather_" + iconSet + "_" + conditionCode, + "drawable", context.getPackageName()); + + if (resId != 0) { + return resId; + } + + // Use the default color set unknown icon + return R.drawable.weather_color_na; + } + + public static Bitmap getWeatherIconBitmap(Context context, String iconSet, + int color, int conditionCode) { + return getWeatherIconBitmap(context, iconSet, color, conditionCode, 0); + } + + public static Bitmap getWeatherIconBitmap(Context context, String iconSet, + int color, int conditionCode, int density) { + boolean isMonoSet = Constants.MONOCHROME.equals(iconSet); + Resources res = null; + int resId = 0; + + if (iconSet.startsWith("ext:")) { + String packageName = iconSet.substring(4); + try { + res = context.getPackageManager().getResourcesForApplication(packageName); + resId = res.getIdentifier("weather_" + conditionCode, "drawable", packageName); + } catch (PackageManager.NameNotFoundException e) { + // fall back to colored icons + iconSet = Constants.COLOR_STD; + } + } + if (resId == 0) { + String identifier = isMonoSet + ? "weather_" + conditionCode : "weather_" + iconSet + "_" + conditionCode; + res = context.getResources(); + resId = res.getIdentifier(identifier, "drawable", context.getPackageName()); + } + + if (resId == 0) { + resId = isMonoSet ? R.drawable.weather_na : R.drawable.weather_color_na; + } + + return getOverlaidBitmap(res, resId, isMonoSet ? color : 0, density); + } + + public static Bitmap getOverlaidBitmap(Resources res, int resId, int color) { + return getOverlaidBitmap(res, resId, color, 0); + } + + public static Bitmap getOverlaidBitmap(Resources res, int resId, int color, int density) { + Bitmap src = getBitmapFromResource(res, resId, density); + if (color == 0 || src == null) { + return src; + } + + final Bitmap dest = Bitmap.createBitmap(src.getWidth(), src.getHeight(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(dest); + final Paint paint = new Paint(); + + // Overlay the selected color and set the imageview + paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + c.drawBitmap(src, 0, 0, paint); + return dest; + } + + public static Bitmap getBitmapFromResource(Resources res, int resId, int density) { + if (density == 0) { + if (D) Log.d(TAG, "Decoding resource id = " + resId + " for default density"); + return BitmapFactory.decodeResource(res, resId); + } + + if (D) Log.d(TAG, "Decoding resource id = " + resId + " for density = " + density); + Drawable d = res.getDrawableForDensity(resId, density); + if (d instanceof BitmapDrawable) { + BitmapDrawable bd = (BitmapDrawable) d; + return bd.getBitmap(); + } + + Bitmap result = Bitmap.createBitmap(d.getIntrinsicWidth(), + d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(result); + d.setBounds(0, 0, result.getWidth(), result.getHeight()); + d.draw(canvas); + canvas.setBitmap(null); + + return result; + } + + public static int getNextHigherDensity(Context context) { + Resources res = context.getResources(); + int density = res.getDisplayMetrics().densityDpi; + + if (density == DisplayMetrics.DENSITY_LOW) { + return DisplayMetrics.DENSITY_MEDIUM; + } else if (density == DisplayMetrics.DENSITY_MEDIUM) { + return DisplayMetrics.DENSITY_HIGH; + } else if (density == DisplayMetrics.DENSITY_HIGH) { + return DisplayMetrics.DENSITY_XHIGH; + } else if (density == DisplayMetrics.DENSITY_XHIGH) { + return DisplayMetrics.DENSITY_XXHIGH; + } else if (density == DisplayMetrics.DENSITY_XXHIGH) { + return DisplayMetrics.DENSITY_XXXHIGH; + } + + // fallback: use current density + return density; + } +} diff --git a/src/com/cyanogenmod/lockclock/misc/Preferences.java b/src/com/cyanogenmod/lockclock/misc/Preferences.java index a6c5c77..8e4c165 100644 --- a/src/com/cyanogenmod/lockclock/misc/Preferences.java +++ b/src/com/cyanogenmod/lockclock/misc/Preferences.java @@ -20,7 +20,10 @@ import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; +import com.cyanogenmod.lockclock.weather.OpenWeatherMapProvider; import com.cyanogenmod.lockclock.weather.WeatherInfo; +import com.cyanogenmod.lockclock.weather.WeatherProvider; +import com.cyanogenmod.lockclock.weather.YahooWeatherProvider; import java.util.Calendar; import java.util.Set; @@ -137,8 +140,8 @@ public class Preferences { return getPrefs(context).getBoolean(Constants.WEATHER_INVERT_LOWHIGH, false); } - public static boolean useAlternateWeatherIcons(Context context) { - return getPrefs(context).getBoolean(Constants.WEATHER_USE_ALTERNATE_ICONS, true); + public static String getWeatherIconSet(Context context) { + return getPrefs(context).getString(Constants.WEATHER_ICONS, "color"); } public static boolean useMetricUnits(Context context) { @@ -154,6 +157,10 @@ public class Preferences { return getPrefs(context).getBoolean(Constants.WEATHER_USE_CUSTOM_LOCATION, false); } + public static void setUseCustomWeatherLocation(Context context, boolean value) { + getPrefs(context).edit().putBoolean(Constants.WEATHER_USE_CUSTOM_LOCATION, value).apply(); + } + public static String customWeatherLocationId(Context context) { return getPrefs(context).getString(Constants.WEATHER_CUSTOM_LOCATION_ID, null); } @@ -166,6 +173,18 @@ public class Preferences { return getPrefs(context).getString(Constants.WEATHER_CUSTOM_LOCATION_CITY, null); } + public static void setCustomWeatherLocationCity(Context context, String city) { + getPrefs(context).edit().putString(Constants.WEATHER_CUSTOM_LOCATION_CITY, city).apply(); + } + + public static WeatherProvider weatherProvider(Context context) { + String name = getPrefs(context).getString(Constants.WEATHER_SOURCE, "yahoo"); + if (name.equals("openweathermap")) { + return new OpenWeatherMapProvider(context); + } + return new YahooWeatherProvider(context); + } + public static void setCachedWeatherInfo(Context context, long timestamp, WeatherInfo data) { SharedPreferences.Editor editor = getPrefs(context).edit(); editor.putLong(Constants.WEATHER_LAST_UPDATE, timestamp); diff --git a/src/com/cyanogenmod/lockclock/misc/WidgetUtils.java b/src/com/cyanogenmod/lockclock/misc/WidgetUtils.java index 465ddc2..e080793 100644 --- a/src/com/cyanogenmod/lockclock/misc/WidgetUtils.java +++ b/src/com/cyanogenmod/lockclock/misc/WidgetUtils.java @@ -23,13 +23,8 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.Bitmap.Config; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -46,22 +41,6 @@ public class WidgetUtils { private static final boolean D = Constants.DEBUG; /** - * Load a resource by Id and overlay with a specified color - */ - public static Bitmap getOverlaidBitmap(Context context, int resId, int overlayColor) { - final Resources res = context.getResources(); - final Bitmap src = BitmapFactory.decodeResource(res, resId); - final Bitmap dest = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Config.ARGB_8888); - Canvas c = new Canvas(dest); - final Paint paint = new Paint(); - - // Overlay the selected color and set the imageview - paint.setColorFilter(new PorterDuffColorFilter(overlayColor, PorterDuff.Mode.SRC_ATOP)); - c.drawBitmap(src, 0, 0, paint); - return dest; - } - - /** * Decide whether to show the small Weather panel */ public static boolean showSmallWidget(Context context, int id, boolean digitalClock, boolean isKeyguard) { @@ -214,7 +193,27 @@ public class WidgetUtils { /** * API level check to see if the new API 17 TextClock is available */ - public static boolean isTextClockAvailable(){ + public static boolean isTextClockAvailable() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; } + + /** + * API level check to see if the new API 19 transparencies are available + */ + public static boolean isTranslucencyAvailable() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + /** + * Networking available check + */ + public static boolean isNetworkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null || !info.isConnected() || !info.isAvailable()) { + if (D) Log.d(TAG, "No network connection is available for weather update"); + return false; + } + return true; + } } diff --git a/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java b/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java index d88809a..6d0992f 100644 --- a/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java +++ b/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java @@ -33,7 +33,6 @@ import android.widget.Toast; import com.cyanogenmod.lockclock.R; import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.weather.WeatherProvider.LocationResult; -import com.cyanogenmod.lockclock.weather.YahooWeatherProvider; import java.util.HashSet; import java.util.List; @@ -112,7 +111,7 @@ public class CustomLocationPreference extends EditTextPreference { @Override protected List<LocationResult> doInBackground(Void... input) { - return new YahooWeatherProvider(getContext()).getLocations(mLocation); + return Preferences.weatherProvider(getContext()).getLocations(mLocation); } @Override diff --git a/src/com/cyanogenmod/lockclock/preference/IconSelectionPreference.java b/src/com/cyanogenmod/lockclock/preference/IconSelectionPreference.java new file mode 100644 index 0000000..87309a2 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/preference/IconSelectionPreference.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2013 The CyanogenMod Project (DvTonder) + * + * 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 com.cyanogenmod.lockclock.preference; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Locale; + +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.TextView; + +import com.cyanogenmod.lockclock.R; + +public class IconSelectionPreference extends DialogPreference implements + AdapterView.OnItemClickListener { + private static final String INTENT_CATEGORY_ICONPACK = "com.dvtonder.chronus.ICON_PACK"; + + private static final String SEARCH_URI = "https://market.android.com/search?q=%s&c=apps"; + private static final String APP_URI = "market://details?id=%s"; + + private static class IconSetDescriptor { + String name; + CharSequence description; + int descriptionResId; + Drawable previewDrawable; + int previewResId; + public IconSetDescriptor(String name, int descriptionResId, + int previewResId) { + this.name = name; + this.descriptionResId = descriptionResId; + this.previewResId = previewResId; + } + public IconSetDescriptor(String packageName, CharSequence description, + Drawable preview) { + this.name = "ext:" + packageName; + this.description = description; + this.previewDrawable = preview; + } + public CharSequence getDescription(Context context) { + if (description != null) { + return description; + } + return context.getString(descriptionResId); + } + @Override + public boolean equals(Object other) { + if (other instanceof IconSetDescriptor) { + IconSetDescriptor o = (IconSetDescriptor) other; + return name.equals(o.name); + } + return false; + } + } + + private static final IconSetDescriptor ICON_SETS[] = new IconSetDescriptor[] { + new IconSetDescriptor("color", R.string.weather_icons_standard, + R.drawable.weather_color_28), + new IconSetDescriptor("mono", R.string.weather_icons_monochrome, + R.drawable.weather_28), + new IconSetDescriptor("vclouds", R.string.weather_icons_vclouds, + R.drawable.weather_vclouds_28) + }; + + private static final IntentFilter PACKAGE_CHANGE_FILTER = new IntentFilter(); + static { + PACKAGE_CHANGE_FILTER.addAction(Intent.ACTION_PACKAGE_ADDED); + PACKAGE_CHANGE_FILTER.addAction(Intent.ACTION_PACKAGE_REMOVED); + PACKAGE_CHANGE_FILTER.addDataScheme("package"); + } + + private IconSetAdapter mAdapter; + private GridView mGrid; + private String mValue; + private String mSelectedValue; + private String mPreviousSelection; + + private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mAdapter.reenumerateIconSets(); + if (getValueIndex(mSelectedValue) == GridView.INVALID_POSITION) { + selectValue(mAdapter.getItem(0).name); + } else { + // index might have changed + selectValue(mSelectedValue); + } + } + }; + + public IconSelectionPreference(Context context, AttributeSet attrs) { + super(context, attrs); + mAdapter = new IconSetAdapter(getContext()); + } + + public CharSequence getEntry() { + int index = getValueIndex(mValue); + if (index != GridView.INVALID_POSITION) { + return mAdapter.getItem(index).getDescription(getContext()); + } + return null; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setNeutralButton(R.string.icon_set_selection_get_more, null); + } + + @Override + protected void showDialog(Bundle state) { + getContext().registerReceiver(mPackageChangeReceiver, PACKAGE_CHANGE_FILTER); + super.showDialog(state); + + AlertDialog d = (AlertDialog) getDialog(); + d.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String uri = String.format(Locale.US, SEARCH_URI, + getContext().getString(R.string.icon_set_store_filter)); + viewUri(getContext(), uri); + } + }); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + int selected = mGrid.getCheckedItemPosition(); + if (positiveResult && selected != GridView.INVALID_POSITION) { + IconSetDescriptor descriptor = mAdapter.getItem(selected); + if (callChangeListener(descriptor.name)) { + mValue = descriptor.name; + persistString(descriptor.name); + } + } + + getContext().unregisterReceiver(mPackageChangeReceiver); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + String defValue = (String) defaultValue; + mValue = restorePersistedValue ? getPersistedString(defValue) : defValue; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + IconSetDescriptor descriptor = mAdapter.getItem(mGrid.getCheckedItemPosition()); + mSelectedValue = descriptor.name; + mPreviousSelection = mSelectedValue; + } + + @Override + protected View onCreateDialogView() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + View view = inflater.inflate(R.layout.icon_style_selection, null); + + mGrid = (GridView) view.findViewById(R.id.icon_list); + mGrid.setAdapter(mAdapter); + mGrid.setOnItemClickListener(this); + + selectValue(mValue); + + return view; + } + + private void selectValue(String value) { + int index = getValueIndex(value); + if (index == GridView.INVALID_POSITION) { + index = 0; + } + mGrid.setItemChecked(index, true); + mSelectedValue = mAdapter.getItem(index).name; + mPreviousSelection = mSelectedValue; + } + + private int getValueIndex(String value) { + int count = mAdapter.getCount(); + for (int i = 0; i < count; i++) { + if (mAdapter.getItem(i).name.equals(value)) { + return i; + } + } + return GridView.INVALID_POSITION; + } + + private static void viewUri(Context context, String uri) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(uri)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(intent); + } + + private static class IconSetAdapter extends ArrayAdapter<IconSetDescriptor> { + private LayoutInflater mInflater; + + public IconSetAdapter(Context context) { + super(context, R.layout.icon_item, 0, populateIconSets(context)); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public void reenumerateIconSets() { + ArrayList<IconSetDescriptor> newSets = populateIconSets(getContext()); + boolean changed = false; + + if (newSets.size() != getCount()) { + changed = true; + } else { + for (int i = 0; i < getCount(); i++) { + if (!newSets.get(i).equals(getItem(i))) { + changed = true; + break; + } + } + } + + if (changed) { + setNotifyOnChange(false); + clear(); + addAll(newSets); + notifyDataSetChanged(); + } + } + + private static ArrayList<IconSetDescriptor> populateIconSets(Context context) { + ArrayList<IconSetDescriptor> result = new ArrayList<IconSetDescriptor>(); + for (IconSetDescriptor desc : ICON_SETS) { + result.add(desc); + } + + PackageManager pm = context.getPackageManager(); + Intent i = new Intent(Intent.ACTION_MAIN); + i.addCategory(INTENT_CATEGORY_ICONPACK); + + HashSet<String> installedIconPacks = new HashSet<String>(); + + for (ResolveInfo info : pm.queryIntentActivities(i, 0)) { + ApplicationInfo appInfo = info.activityInfo.applicationInfo; + try { + Resources res = pm.getResourcesForApplication(appInfo); + int previewResId = res.getIdentifier("weather_28", "drawable", appInfo.packageName); + Drawable preview = previewResId != 0 ? res.getDrawable(previewResId) : null; + result.add(new IconSetDescriptor(appInfo.packageName, + appInfo.loadLabel(pm), preview)); + installedIconPacks.add(appInfo.packageName.toLowerCase(Locale.US)); + } catch (PackageManager.NameNotFoundException e) { + // shouldn't happen, ignore package + } + } + return result; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.icon_item, parent, false); + } + + IconSetDescriptor descriptor = getItem(position); + ImageView preview = (ImageView) convertView.findViewById(R.id.preview); + TextView name = (TextView) convertView.findViewById(R.id.name); + + if (descriptor.previewDrawable != null) { + preview.setImageDrawable(descriptor.previewDrawable); + } else { + preview.setImageResource(descriptor.previewResId); + } + name.setText(descriptor.getDescription(getContext())); + return convertView; + } + } +} diff --git a/src/com/cyanogenmod/lockclock/preference/Preferences.java b/src/com/cyanogenmod/lockclock/preference/Preferences.java index bb9faa2..f60ab4d 100755 --- a/src/com/cyanogenmod/lockclock/preference/Preferences.java +++ b/src/com/cyanogenmod/lockclock/preference/Preferences.java @@ -16,6 +16,7 @@ package com.cyanogenmod.lockclock.preference; +import android.annotation.SuppressLint; import android.appwidget.AppWidgetManager; import android.content.Intent; import android.preference.PreferenceActivity; @@ -77,4 +78,14 @@ public class Preferences extends PreferenceActivity { setResult(result, new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mNewWidgetId)); } } + + /** + * This is required to be able to build with API level 19 + */ + @SuppressLint("Override") + @Override + public boolean isValidFragment(String fragmentName) { + // Assume a valid fragment name at all times + return true; + } } diff --git a/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java b/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java index 327b3e1..9b06246 100644 --- a/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java +++ b/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java @@ -58,6 +58,8 @@ public class WeatherPreferences extends PreferenceFragment implements private ListPreference mFontColor; private ListPreference mTimestampFontColor; private CheckBoxPreference mUseMetric; + private IconSelectionPreference mIconSet; + private CheckBoxPreference mUseCustomlocation; private Context mContext; private ContentResolver mResolver; @@ -73,16 +75,15 @@ public class WeatherPreferences extends PreferenceFragment implements // Load items that need custom summaries etc. mUseCustomLoc = (CheckBoxPreference) findPreference(Constants.WEATHER_USE_CUSTOM_LOCATION); mCustomWeatherLoc = (EditTextPreference) findPreference(Constants.WEATHER_CUSTOM_LOCATION_CITY); - mFontColor = (ListPreference) findPreference(Constants.WEATHER_FONT_COLOR); mTimestampFontColor = (ListPreference) findPreference(Constants.WEATHER_TIMESTAMP_FONT_COLOR); - + mIconSet = (IconSelectionPreference) findPreference(Constants.WEATHER_ICONS); mUseMetric = (CheckBoxPreference) findPreference(Constants.WEATHER_USE_METRIC); + mUseCustomlocation = (CheckBoxPreference) findPreference(Constants.WEATHER_USE_CUSTOM_LOCATION); // Show a warning if location manager is disabled and there is no custom location set - if (!Settings.Secure.isLocationProviderEnabled(mResolver, - LocationManager.NETWORK_PROVIDER) - && !mUseCustomLoc.isChecked()) { + LocationManager lm = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE); + if (!lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) && !mUseCustomLoc.isChecked()) { showDialog(); } } @@ -93,6 +94,7 @@ public class WeatherPreferences extends PreferenceFragment implements getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); updateLocationSummary(); updateFontColorsSummary(); + updateIconSetSummary(); } @Override @@ -109,12 +111,16 @@ public class WeatherPreferences extends PreferenceFragment implements pref.setSummary(listPref.getEntry()); } + boolean needWeatherUpdate = false; + boolean forceWeatherUpdate = false; + if (pref == mUseCustomLoc || pref == mCustomWeatherLoc) { updateLocationSummary(); } - boolean needWeatherUpdate = false; - boolean forceWeatherUpdate = false; + if (pref == mIconSet) { + updateIconSetSummary(); + } if (pref == mUseMetric) { // The display format of the temperatures have changed @@ -122,20 +128,23 @@ public class WeatherPreferences extends PreferenceFragment implements forceWeatherUpdate = true; } - for (String k : LOCATION_PREF_KEYS) { - if (TextUtils.equals(key, k)) { - // location pref has changed -> clear out location id cache - Preferences.setCachedLocationId(mContext, null); - forceWeatherUpdate = true; - break; - } + // If the weather source has changes, invalidate the custom location settings and change + // back to GeoLocation to force the user to specify a new custom location if needed + if (TextUtils.equals(key, Constants.WEATHER_SOURCE)) { + Preferences.setCustomWeatherLocationId(mContext, null); + Preferences.setCustomWeatherLocationCity(mContext, null); + Preferences.setUseCustomWeatherLocation(mContext, false); + mUseCustomlocation.setChecked(false); + updateLocationSummary(); } - for (String k : WEATHER_REFRESH_KEYS) { - if (TextUtils.equals(key, k)) { - needWeatherUpdate = true; - break; - } + if (key.equals(Constants.WEATHER_USE_CUSTOM_LOCATION) + || key.equals(Constants.WEATHER_CUSTOM_LOCATION_CITY)) { + forceWeatherUpdate = true; + } + + if (key.equals(Constants.SHOW_WEATHER) || key.equals(Constants.WEATHER_REFRESH_INTERVAL)) { + needWeatherUpdate = true; } if (Constants.DEBUG) { @@ -200,4 +209,10 @@ public class WeatherPreferences extends PreferenceFragment implements mTimestampFontColor.setSummary(mTimestampFontColor.getEntry()); } } + + private void updateIconSetSummary() { + if (mIconSet != null) { + mIconSet.setSummary(mIconSet.getEntry()); + } + } } diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java new file mode 100644 index 0000000..2409cb8 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2013 David van Tonder + * + * 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 com.cyanogenmod.lockclock.weather; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.KeyguardManager; +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; + +import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.R; + +public class ForecastActivity extends Activity implements OnClickListener { + private static final String TAG = "ForecastActivity"; + + private BroadcastReceiver mUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Stop the animation + ImageView view = (ImageView) findViewById(R.id.weather_refresh); + view.setAnimation(null); + + if (!intent.getBooleanExtra(WeatherUpdateService.EXTRA_UPDATE_CANCELLED, false)) { + updateForecastPanel(); + } + } + }; + + @SuppressLint("InlinedApi") + @Override + public void onCreate(Bundle savedInstanceState) { + // If we are in keyguard, override the default transparent theme + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + boolean locked = km.isKeyguardLocked(); + if (locked) { + if (WidgetUtils.isTranslucencyAvailable()) { + setTheme(android.R.style.Theme_Holo_NoActionBar_TranslucentDecor); + } else { + setTheme(android.R.style.Theme_Holo_NoActionBar); + } + } + super.onCreate(savedInstanceState); + + // Get the window ready + Window window = getWindow(); + requestWindowFeature(Window.FEATURE_NO_TITLE); + if (locked) { + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + final WallpaperManager wallpaperManager = WallpaperManager.getInstance(this); + final Drawable wallpaperDrawable = wallpaperManager.getFastDrawable(); + window.setBackgroundDrawable(wallpaperDrawable); + } else if (WidgetUtils.isTranslucencyAvailable()) { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + + registerReceiver(mUpdateReceiver, new IntentFilter(WeatherUpdateService.ACTION_UPDATE_FINISHED)); + updateForecastPanel(); + } + + @Override + protected void onDestroy() { + unregisterReceiver(mUpdateReceiver); + super.onDestroy(); + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + finish(); + } + + private void updateForecastPanel() { + // Get the forecasts data + WeatherInfo weather = Preferences.getCachedWeatherInfo(this); + if (weather == null) { + Log.e(TAG, "Error retrieving forecast data, exiting"); + finish(); + return; + } + + View fullLayout = ForecastBuilder.buildFullPanel(this, R.layout.forecast_activity, weather); + setContentView(fullLayout); + fullLayout.requestFitSystemWindows(); + + // Register an onClickListener on Weather refresh + findViewById(R.id.weather_refresh).setOnClickListener(this); + + // Register an onClickListener on the fake done button + findViewById(R.id.button).setOnClickListener(this); + } + + @Override + public void onClick(View v) { + if (v.getId() != R.id.button) { + // Setup anim with desired properties and start the animation + ImageView view = (ImageView) findViewById(R.id.weather_refresh); + RotateAnimation anim = new RotateAnimation(0.0f, 360.0f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f); + anim.setInterpolator(new LinearInterpolator()); + anim.setRepeatCount(Animation.INFINITE); + anim.setDuration(700); + view.startAnimation(anim); + + Intent i = new Intent(this, WeatherUpdateService.class); + i.setAction(WeatherUpdateService.ACTION_FORCE_UPDATE); + startService(i); + } else { + finish(); + } + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java new file mode 100644 index 0000000..6f8da53 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2013 David van Tonder + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use context 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 com.cyanogenmod.lockclock.weather; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.WebView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.cyanogenmod.lockclock.misc.IconUtils; +import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; + +public class ForecastBuilder { + private static final String TAG = "ForecastBuilder"; + + /** + * This method is used to build the full current conditions and horizontal forecasts + * panels + * + * @param context + * @param w = the Weather info object that contains the forecast data + * @return = a built view that can be displayed + */ + @SuppressLint("SetJavaScriptEnabled") + public static View buildFullPanel(Context context, int resourceId, WeatherInfo w) { + + // Load some basic settings + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int color = Preferences.weatherFontColor(context); + boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + + View view = inflater.inflate(resourceId, null); + + // Set the weather source + TextView weatherSource = (TextView) view.findViewById(R.id.weather_source); + weatherSource.setText(Preferences.weatherProvider(context).getNameResourceId()); + + // Set the current conditions + // Weather Image + ImageView weatherImage = (ImageView) view.findViewById(R.id.weather_image); + String iconsSet = Preferences.getWeatherIconSet(context); + weatherImage.setImageBitmap(w.getConditionBitmap(iconsSet, color, + IconUtils.getNextHigherDensity(context))); + + // Weather Condition + TextView weatherCondition = (TextView) view.findViewById(R.id.weather_condition); + weatherCondition.setText(w.getCondition()); + + // Weather Temps + TextView weatherTemp = (TextView) view.findViewById(R.id.weather_temp); + weatherTemp.setText(w.getFormattedTemperature()); + + // City + TextView city = (TextView) view.findViewById(R.id.weather_city); + city.setText(w.getCity()); + + // Weather Update Time + Date lastUpdate = w.getTimestamp(); + StringBuilder sb = new StringBuilder(); + sb.append(DateFormat.format("E", lastUpdate)); + sb.append(" "); + sb.append(DateFormat.getTimeFormat(context).format(lastUpdate)); + TextView updateTime = (TextView) view.findViewById(R.id.update_time); + updateTime.setText(sb.toString()); + updateTime.setVisibility(Preferences.showWeatherTimestamp(context) ? View.VISIBLE : View.GONE); + + // Weather Temps Panel additional items + final String low = w.getFormattedLow(); + final String high = w.getFormattedHigh(); + TextView weatherLowHigh = (TextView) view.findViewById(R.id.weather_low_high); + weatherLowHigh.setText(invertLowHigh ? high + " | " + low : low + " | " + high); + + // Get things ready + LinearLayout forecastView = (LinearLayout) view.findViewById(R.id.forecast_view); + final View progressIndicator = view.findViewById(R.id.progress_indicator); + + // Build the forecast panel + if (buildSmallPanel(context, forecastView, w)) { + // Success, hide the progress container + progressIndicator.setVisibility(View.GONE); + } + + return view; + } + + /** + * This method is used to build the small, horizontal forecasts panel + * @param context + * @param smallPanel = a horizontal linearlayout that will contain the forecasts + * @param w = the Weather info object that contains the forecast data + */ + public static boolean buildSmallPanel(Context context, LinearLayout smallPanel, WeatherInfo w) { + if (smallPanel == null) { + Log.d(TAG, "Invalid view passed"); + return false; + } + + // Get things ready + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int color = Preferences.weatherFontColor(context); + boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + + ArrayList<DayForecast> forecasts = w.getForecasts(); + if (forecasts == null || forecasts.size() <= 1) { + smallPanel.setVisibility(View.GONE); + return false; + } + + TimeZone MyTimezone = TimeZone.getDefault(); + Calendar calendar = new GregorianCalendar(MyTimezone); + + // Iterate through the forecasts + for (DayForecast d : forecasts) { + // Load the views + View forecastItem = inflater.inflate(R.layout.forecast_item, null); + + // The day of the week + TextView day = (TextView) forecastItem.findViewById(R.id.forecast_day); + day.setText(calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.getDefault())); + calendar.roll(Calendar.DAY_OF_WEEK, true); + + // Weather Image + ImageView image = (ImageView) forecastItem.findViewById(R.id.weather_image); + String iconsSet = Preferences.getWeatherIconSet(context); + int resId = d.getConditionResource(context, iconsSet); + if (resId != 0) { + image.setImageResource(resId); + } else { + image.setImageBitmap(d.getConditionBitmap(context, iconsSet, color)); + } + + // Temperatures + String dayLow = d.getFormattedLow(); + String dayHigh = d.getFormattedHigh(); + TextView temps = (TextView) forecastItem.findViewById(R.id.weather_temps); + temps.setText(invertLowHigh ? dayHigh + " " + dayLow : dayLow + " " + dayHigh); + + // Add the view + smallPanel.addView(forecastItem, + new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + } + return true; + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java b/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java index 957d6dc..60723fa 100755 --- a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java +++ b/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java @@ -38,7 +38,7 @@ public class HttpRetriever { return EntityUtils.toString(entity); } } catch (IOException e) { - Log.e(TAG, "Couldn't retrieve data", e); + Log.e(TAG, "Couldn't retrieve data from url " + url, e); } return null; } diff --git a/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java b/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java new file mode 100644 index 0000000..808077c --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java @@ -0,0 +1,311 @@ +package com.cyanogenmod.lockclock.weather; + +import java.util.*; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.location.Location; +import android.net.Uri; +import android.util.Log; + +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; + +public class OpenWeatherMapProvider implements WeatherProvider { + private static final String TAG = "OpenWeatherMapProvider"; + + private static final int FORECAST_DAYS = 5; + private static final String SELECTION_LOCATION = "lat=%f&lon=%f"; + private static final String SELECTION_ID = "id=%s"; + + private static final String URL_LOCATION = + "http://api.openweathermap.org/data/2.5/find?q=%s&mode=json&lang=%s"; + private static final String URL_WEATHER = + "http://api.openweathermap.org/data/2.5/weather?%s&mode=json&units=%s&lang=%s"; + private static final String URL_FORECAST = + "http://api.openweathermap.org/data/2.5/forecast/daily?" + + "%s&mode=json&units=%s&lang=%s&cnt=" + FORECAST_DAYS; + + private Context mContext; + + public OpenWeatherMapProvider(Context context) { + mContext = context; + } + + @Override + public int getNameResourceId() { + return R.string.weather_source_openweathermap; + } + + @Override + public List<LocationResult> getLocations(String input) { + String url = String.format(URL_LOCATION, Uri.encode(input), getLanguageCode()); + String response = HttpRetriever.retrieve(url); + if (response == null) { + return null; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "URL = " + url + " returning a response of " + response); + } + + try { + JSONArray jsonResults = new JSONObject(response).getJSONArray("list"); + ArrayList<LocationResult> results = new ArrayList<LocationResult>(); + int count = jsonResults.length(); + + for (int i = 0; i < count; i++) { + JSONObject result = jsonResults.getJSONObject(i); + LocationResult location = new LocationResult(); + + location.id = result.getString("id"); + location.city = result.getString("name"); + location.countryId = result.getJSONObject("sys").getString("country"); + results.add(location); + } + + return results; + } catch (JSONException e) { + Log.w(TAG, "Received malformed location data (input=" + input + ")", e); + } + + return null; + } + + public WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metric) { + String selection = String.format(Locale.US, SELECTION_ID, id); + return handleWeatherRequest(selection, localizedCityName, metric); + } + + public WeatherInfo getWeatherInfo(Location location, boolean metric) { + String selection = String.format(Locale.US, SELECTION_LOCATION, + location.getLatitude(), location.getLongitude()); + return handleWeatherRequest(selection, null, metric); + } + + private WeatherInfo handleWeatherRequest(String selection, + String localizedCityName, boolean metric) { + String units = metric ? "metric" : "imperial"; + String locale = getLanguageCode(); + String conditionUrl = String.format(Locale.US, URL_WEATHER, selection, units, locale); + String conditionResponse = HttpRetriever.retrieve(conditionUrl); + if (conditionResponse == null) { + return null; + } + + String forecastUrl = String.format(Locale.US, URL_FORECAST, selection, units, locale); + String forecastResponse = HttpRetriever.retrieve(forecastUrl); + if (forecastResponse == null) { + return null; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "URL = " + conditionUrl + " returning a response of " + conditionResponse); + } + + try { + JSONObject conditions = new JSONObject(conditionResponse); + JSONObject weather = conditions.getJSONArray("weather").getJSONObject(0); + JSONObject conditionData = conditions.getJSONObject("main"); + JSONObject windData = conditions.getJSONObject("wind"); + ArrayList<DayForecast> forecasts = + parseForecasts(new JSONObject(forecastResponse).getJSONArray("list")); + int speedUnitResId = metric ? R.string.weather_kph : R.string.weather_mph; + if (localizedCityName == null) { + localizedCityName = conditions.getString("name"); + } + + WeatherInfo w = new WeatherInfo(mContext, conditions.getString("id"), localizedCityName, + /* condition */ weather.getString("main"), + /* conditionCode */ mapConditionIconToCode( + weather.getString("icon"), weather.getInt("id")), + /* temperature */ (float) conditionData.getDouble("temp"), + /* tempUnit */ metric ? "C" : "F", + /* humidity */ (float) conditionData.getDouble("humidity"), + /* wind */ (float) windData.getDouble("speed"), + /* windDir */ windData.getInt("deg"), + /* speedUnit */ mContext.getString(speedUnitResId), + forecasts, + System.currentTimeMillis()); + + Log.d(TAG, "Weather updated: " + w); + return w; + } catch (JSONException e) { + Log.w(TAG, "Received malformed weather data (selection = " + selection + + ", lang = " + locale + ")", e); + } + + return null; + } + + private ArrayList<DayForecast> parseForecasts(JSONArray forecasts) throws JSONException { + ArrayList<DayForecast> result = new ArrayList<DayForecast>(); + int count = forecasts.length(); + + if (count == 0) { + throw new JSONException("Empty forecasts array"); + } + for (int i = 0; i < count; i++) { + JSONObject forecast = forecasts.getJSONObject(i); + JSONObject temperature = forecast.getJSONObject("temp"); + JSONObject data = forecast.getJSONArray("weather").getJSONObject(0); + DayForecast item = new DayForecast( + /* low */ (float) temperature.getDouble("min"), + /* high */ (float) temperature.getDouble("max"), + /* condition */ data.getString("main"), + /* conditionCode */ mapConditionIconToCode( + data.getString("icon"), data.getInt("id"))); + result.add(item); + } + + return result; + } + + private static final HashMap<String, Integer> ICON_MAPPING = new HashMap<String, Integer>(); + static { + ICON_MAPPING.put("01d", 32); + ICON_MAPPING.put("01n", 31); + ICON_MAPPING.put("02d", 30); + ICON_MAPPING.put("02n", 29); + ICON_MAPPING.put("03d", 26); + ICON_MAPPING.put("03n", 26); + ICON_MAPPING.put("04d", 28); + ICON_MAPPING.put("04n", 27); + ICON_MAPPING.put("09d", 12); + ICON_MAPPING.put("09n", 11); + ICON_MAPPING.put("10d", 40); + ICON_MAPPING.put("10n", 45); + ICON_MAPPING.put("11d", 4); + ICON_MAPPING.put("11n", 4); + ICON_MAPPING.put("13d", 16); + ICON_MAPPING.put("13n", 16); + ICON_MAPPING.put("50d", 21); + ICON_MAPPING.put("50n", 20); + } + + private int mapConditionIconToCode(String icon, int conditionId) { + + // First, use condition ID for specific cases + switch (conditionId) { + // Thunderstorms + case 202: // thunderstorm with heavy rain + case 232: // thunderstorm with heavy drizzle + case 211: // thunderstorm + return 4; + case 212: // heavy thunderstorm + return 3; + case 221: // ragged thunderstorm + case 231: // thunderstorm with drizzle + case 201: // thunderstorm with rain + return 38; + case 230: // thunderstorm with light drizzle + case 200: // thunderstorm with light rain + case 210: // light thunderstorm + return 37; + + // Drizzle + case 300: // light intensity drizzle + case 301: // drizzle + case 302: // heavy intensity drizzle + case 310: // light intensity drizzle rain + case 311: // drizzle rain + case 312: // heavy intensity drizzle rain + case 313: // shower rain and drizzle + case 314: // heavy shower rain and drizzle + case 321: // shower drizzle + return 9; + + // Rain + case 500: // light rain + case 501: // moderate rain + case 520: // light intensity shower rain + case 521: // shower rain + case 531: // ragged shower rain + return 11; + case 502: // heavy intensity rain + case 503: // very heavy rain + case 504: // extreme rain + case 522: // heavy intensity shower rain + return 12; + case 511: // freezing rain + return 10; + + // Snow + case 600: case 620: return 14; // light snow + case 601: case 621: return 16; // snow + case 602: case 622: return 41; // heavy snow + case 611: case 612: return 18; // sleet + case 615: case 616: return 5; // rain and snow + + // Atmosphere + case 741: // fog + return 20; + case 711: // smoke + case 762: // volcanic ash + return 22; + case 701: // mist + case 721: // haze + return 21; + case 731: // sand/dust whirls + case 751: // sand + case 761: // dust + return 19; + case 771: // squalls + return 23; + case 781: // tornado + return 0; + + // Extreme + case 900: return 0; // tornado + case 901: return 1; // tropical storm + case 902: return 2; // hurricane + case 903: return 25; // cold + case 904: return 36; // hot + case 905: return 24; // windy + case 906: return 17; // hail + } + + // Not yet handled - Use generic icon mapping + Integer condition = ICON_MAPPING.get(icon); + if (condition != null) { + return condition; + } + + return -1; + } + + private static final HashMap<String, String> LANGUAGE_CODE_MAPPING = new HashMap<String, String>(); + static { + LANGUAGE_CODE_MAPPING.put("bg-", "bg"); + LANGUAGE_CODE_MAPPING.put("de-", "de"); + LANGUAGE_CODE_MAPPING.put("es-", "sp"); + LANGUAGE_CODE_MAPPING.put("fi-", "fi"); + LANGUAGE_CODE_MAPPING.put("fr-", "fr"); + LANGUAGE_CODE_MAPPING.put("it-", "it"); + LANGUAGE_CODE_MAPPING.put("nl-", "nl"); + LANGUAGE_CODE_MAPPING.put("pl-", "pl"); + LANGUAGE_CODE_MAPPING.put("pt-", "pt"); + LANGUAGE_CODE_MAPPING.put("ro-", "ro"); + LANGUAGE_CODE_MAPPING.put("ru-", "ru"); + LANGUAGE_CODE_MAPPING.put("se-", "se"); + LANGUAGE_CODE_MAPPING.put("tr-", "tr"); + LANGUAGE_CODE_MAPPING.put("uk-", "ua"); + LANGUAGE_CODE_MAPPING.put("zh-CN", "zh_cn"); + LANGUAGE_CODE_MAPPING.put("zh-TW", "zh_tw"); + } + private String getLanguageCode() { + Locale locale = mContext.getResources().getConfiguration().locale; + String selector = locale.getLanguage() + "-" + locale.getCountry(); + + for (Map.Entry<String, String> entry : LANGUAGE_CODE_MAPPING.entrySet()) { + if (selector.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + + return "en"; + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java b/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java index a857734..b7c09d5 100755 --- a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java @@ -21,9 +21,10 @@ import android.content.res.Resources; import android.graphics.Bitmap; import com.cyanogenmod.lockclock.R; -import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.misc.IconUtils; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Date; public class WeatherInfo { @@ -33,27 +34,24 @@ public class WeatherInfo { private String id; private String city; - private String forecastDate; private String condition; private int conditionCode; private float temperature; - private float lowTemperature; - private float highTemperature; private String tempUnit; private float humidity; private float wind; private int windDirection; private String speedUnit; private long timestamp; + private ArrayList<DayForecast> forecasts; public WeatherInfo(Context context, String id, - String city, String fdate, String condition, int conditionCode, - float temp, float low, float high, String tempUnit, float humidity, - float wind, int windDir, String speedUnit, long timestamp) { + String city, String condition, int conditionCode, float temp, + String tempUnit, float humidity, float wind, int windDir, + String speedUnit, ArrayList<DayForecast> forecasts, long timestamp) { this.mContext = context.getApplicationContext(); this.id = id; this.city = city; - this.forecastDate = fdate; this.condition = condition; this.conditionCode = conditionCode; this.humidity = humidity; @@ -62,27 +60,57 @@ public class WeatherInfo { this.speedUnit = speedUnit; this.timestamp = timestamp; this.temperature = temp; - this.lowTemperature = low; - this.highTemperature = high; this.tempUnit = tempUnit; + this.forecasts = forecasts; } - public int getConditionResource() { - final Resources res = mContext.getResources(); - final int resId = res.getIdentifier("weather2_" + conditionCode, "drawable", mContext.getPackageName()); - if (resId != 0) { - return resId; + public static class DayForecast { + public final float low, high; + public final int conditionCode; + public final String condition; + + public DayForecast(float low, float high, String condition, int conditionCode) { + this.low = low; + this.high = high; + this.condition = condition; + this.conditionCode = conditionCode; + } + + public String getFormattedLow() { + return getFormattedValue(low, "\u00b0"); } - return R.drawable.weather2_na; - } - public Bitmap getConditionBitmap(int color) { - final Resources res = mContext.getResources(); - int resId = res.getIdentifier("weather_" + conditionCode, "drawable", mContext.getPackageName()); - if (resId == 0) { - resId = R.drawable.weather_na; + public String getFormattedHigh() { + return getFormattedValue(high, "\u00b0"); } - return WidgetUtils.getOverlaidBitmap(mContext, resId, color); + + public int getConditionResource(Context context, String set) { + return IconUtils.getWeatherIconResource(context, set, conditionCode); + } + + public Bitmap getConditionBitmap(Context context, String set, int color) { + return IconUtils.getWeatherIconBitmap(context, set, color, conditionCode); + } + + public Bitmap getConditionBitmap(Context context, String set, int color, int density) { + return IconUtils.getWeatherIconBitmap(context, set, color, conditionCode, density); + } + + public String getCondition(Context context) { + return WeatherInfo.getCondition(context, conditionCode, condition); + } + } + + public int getConditionResource(String set) { + return IconUtils.getWeatherIconResource(mContext, set, conditionCode); + } + + public Bitmap getConditionBitmap(String set, int color) { + return IconUtils.getWeatherIconBitmap(mContext, set, color, conditionCode); + } + + public Bitmap getConditionBitmap(String set, int color, int density) { + return IconUtils.getWeatherIconBitmap(mContext, set, color, conditionCode, density); } public String getId() { @@ -94,8 +122,12 @@ public class WeatherInfo { } public String getCondition() { - final Resources res = mContext.getResources(); - final int resId = res.getIdentifier("weather_" + conditionCode, "string", mContext.getPackageName()); + return getCondition(mContext, conditionCode, condition); + } + + private static String getCondition(Context context, int conditionCode, String condition) { + final Resources res = context.getResources(); + final int resId = res.getIdentifier("weather_" + conditionCode, "string", context.getPackageName()); if (resId != 0) { return res.getString(resId); } @@ -106,23 +138,27 @@ public class WeatherInfo { return new Date(timestamp); } - private String getFormattedValue(float value, String unit) { - if (Float.isNaN(highTemperature)) { + private static String getFormattedValue(float value, String unit) { + if (Float.isNaN(value)) { return "-"; } - return sNoDigitsFormat.format(value) + unit; + String formatted = sNoDigitsFormat.format(value); + if (formatted.equals("-0")) { + formatted = "0"; + } + return formatted + unit; } public String getFormattedTemperature() { - return getFormattedValue(temperature, "°" + tempUnit); + return getFormattedValue(temperature, "\u00b0" + tempUnit); } public String getFormattedLow() { - return getFormattedValue(lowTemperature, "°"); + return forecasts.get(0).getFormattedLow(); } public String getFormattedHigh() { - return getFormattedValue(highTemperature, "°"); + return forecasts.get(0).getFormattedHigh(); } public String getFormattedHumidity() { @@ -153,6 +189,10 @@ public class WeatherInfo { return mContext.getString(resId); } + public ArrayList<DayForecast> getForecasts() { + return forecasts; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -178,6 +218,20 @@ public class WeatherInfo { builder.append(getFormattedWindSpeed()); builder.append(" at "); builder.append(getWindDirection()); + if (forecasts.size() > 0) { + builder.append(", forecasts:"); + } + for (int i = 0; i < forecasts.size(); i++) { + DayForecast d = forecasts.get(i); + if (i != 0) { + builder.append(";"); + } + builder.append(" day ").append(i + 1).append(": "); + builder.append("high ").append(d.getFormattedHigh()); + builder.append(", low ").append(d.getFormattedLow()); + builder.append(", ").append(d.condition); + builder.append("(").append(d.conditionCode).append(")"); + } return builder.toString(); } @@ -185,52 +239,88 @@ public class WeatherInfo { StringBuilder builder = new StringBuilder(); builder.append(id).append('|'); builder.append(city).append('|'); - builder.append(forecastDate).append('|'); builder.append(condition).append('|'); builder.append(conditionCode).append('|'); builder.append(temperature).append('|'); - builder.append(lowTemperature).append('|'); - builder.append(highTemperature).append('|'); builder.append(tempUnit).append('|'); builder.append(humidity).append('|'); builder.append(wind).append('|'); builder.append(windDirection).append('|'); builder.append(speedUnit).append('|'); - builder.append(timestamp); + builder.append(timestamp).append('|'); + serializeForecasts(builder); return builder.toString(); } + private void serializeForecasts(StringBuilder builder) { + builder.append(forecasts.size()); + for (DayForecast d : forecasts) { + builder.append(';'); + builder.append(d.high).append(';'); + builder.append(d.low).append(';'); + builder.append(d.condition).append(';'); + builder.append(d.conditionCode); + } + } + public static WeatherInfo fromSerializedString(Context context, String input) { if (input == null) { return null; } String[] parts = input.split("\\|"); - if (parts == null || parts.length != 14) { + if (parts == null || parts.length != 12) { return null; } int conditionCode, windDirection; long timestamp; - float temperature, low, high, humidity, wind; + float temperature, humidity, wind; + String[] forecastParts = parts[11].split(";"); + int forecastItems; + ArrayList<DayForecast> forecasts = new ArrayList<DayForecast>(); + // Parse the core data try { - conditionCode = Integer.parseInt(parts[4]); - temperature = Float.parseFloat(parts[5]); - low = Float.parseFloat(parts[6]); - high = Float.parseFloat(parts[7]); - humidity = Float.parseFloat(parts[9]); - wind = Float.parseFloat(parts[10]); - windDirection = Integer.parseInt(parts[11]); - timestamp = Long.parseLong(parts[13]); + conditionCode = Integer.parseInt(parts[3]); + temperature = Float.parseFloat(parts[4]); + humidity = Float.parseFloat(parts[6]); + wind = Float.parseFloat(parts[7]); + windDirection = Integer.parseInt(parts[8]); + timestamp = Long.parseLong(parts[10]); + forecastItems = forecastParts == null ? 0 : Integer.parseInt(forecastParts[0]); } catch (NumberFormatException e) { return null; } + if (forecastItems == 0 || forecastParts.length != 4 * forecastItems + 1) { + return null; + } + + // Parse the forecast data + try { + for (int item = 0; item < forecastItems; item ++) { + int offset = item * 4 + 1; + DayForecast day = new DayForecast( + /* low */ Float.parseFloat(forecastParts[offset + 1]), + /* high */ Float.parseFloat(forecastParts[offset]), + /* condition */ forecastParts[offset + 2], + /* conditionCode */ Integer.parseInt(forecastParts[offset + 3])); + if (!Float.isNaN(day.low) && !Float.isNaN(day.high) && day.conditionCode >= 0) { + forecasts.add(day); + } + } + } catch (NumberFormatException ignored) { + } + + if (forecasts.isEmpty()) { + return null; + } + return new WeatherInfo(context, - /* id */ parts[0], /* city */ parts[1], /* date */ parts[2], - /* condition */ parts[3], conditionCode, temperature, low, high, - /* tempUnit */ parts[8], humidity, wind, windDirection, - /* speedUnit */ parts[12], timestamp); + /* id */ parts[0], /* city */ parts[1], /* condition */ parts[2], + conditionCode, temperature, /* tempUnit */ parts[5], + humidity, wind, windDirection, /* speedUnit */ parts[9], + /* forecasts */ forecasts, timestamp); } } diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java b/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java index 15c8aff..70fbf42 100644 --- a/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java @@ -27,11 +27,13 @@ public interface WeatherProvider { public String postal; public String countryId; public String country; - }; + } List<LocationResult> getLocations(String input); - WeatherInfo getWeatherInfo(String id, String localizedCityName); + WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metricUnits); - WeatherInfo getWeatherInfo(Location location); -}; + WeatherInfo getWeatherInfo(Location location, boolean metricUnits); + + int getNameResourceId(); +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java index 94fca71..f38c046 100755 --- a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java @@ -26,19 +26,19 @@ import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import com.cyanogenmod.lockclock.ClockWidgetProvider; import com.cyanogenmod.lockclock.misc.Constants; import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; import java.util.Date; @@ -47,6 +47,15 @@ public class WeatherUpdateService extends Service { private static final boolean D = Constants.DEBUG; public static final String ACTION_FORCE_UPDATE = "com.cyanogenmod.lockclock.action.FORCE_WEATHER_UPDATE"; + private static final String ACTION_CANCEL_LOCATION_UPDATE = + "com.cyanogenmod.lockclock.action.CANCEL_LOCATION_UPDATE"; + + // Broadcast action for end of update + public static final String ACTION_UPDATE_FINISHED = "com.cyanogenmod.lockclock.action.WEATHER_UPDATE_FINISHED"; + public static final String EXTRA_UPDATE_CANCELLED = "update_cancelled"; + + private static final long LOCATION_REQUEST_TIMEOUT = 5L * 60L * 1000L; // request for at most 5 minutes + private static final long OUTDATED_LOCATION_THRESHOLD_MILLIS = 10L * 60L * 1000L; // 10 minutes private WeatherUpdateTask mTask; @@ -62,18 +71,26 @@ public class WeatherUpdateService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { if (D) Log.v(TAG, "Got intent " + intent); - if (mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED) { + boolean active = mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED; + + if (ACTION_CANCEL_LOCATION_UPDATE.equals(intent.getAction())) { + WeatherLocationListener.cancel(this); + if (!active) { + stopSelf(); + } + return START_NOT_STICKY; + } + + if (active) { if (D) Log.v(TAG, "Weather update is still active, not starting new update"); return START_REDELIVER_INTENT; } boolean force = ACTION_FORCE_UPDATE.equals(intent.getAction()); - if (force) { - Preferences.setCachedWeatherInfo(this, 0, null); - } if (!shouldUpdate(force)) { Log.d(TAG, "Service started, but shouldn't update ... stopping"); stopSelf(); + sendCancelledBroadcast(); return START_NOT_STICKY; } @@ -83,6 +100,12 @@ public class WeatherUpdateService extends Service { return START_REDELIVER_INTENT; } + private void sendCancelledBroadcast() { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, true); + sendBroadcast(finishedIntent); + } + @Override public IBinder onBind(Intent intent) { return null; @@ -97,25 +120,16 @@ public class WeatherUpdateService extends Service { } private boolean shouldUpdate(boolean force) { - ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - - if (info == null || !info.isConnected()) { - if (D) Log.d(TAG, "No network connection is available for weather update"); - return false; - } - - if (!Preferences.showWeather(this)) { - if (D) Log.v(TAG, "Weather isn't shown, skip update"); - return false; - } - long interval = Preferences.weatherRefreshIntervalInMs(this); if (interval == 0 && !force) { if (D) Log.v(TAG, "Interval set to manual and update not forced, skip update"); return false; } + if (force) { + Preferences.setCachedWeatherInfo(this, 0, null); + } + long now = System.currentTimeMillis(); long lastUpdate = Preferences.lastWeatherUpdateTimestamp(this); long due = lastUpdate + interval; @@ -127,7 +141,7 @@ public class WeatherUpdateService extends Service { return false; } - return true; + return WidgetUtils.isNetworkAvailable(this); } private class WeatherUpdateTask extends AsyncTask<Void, Void, WeatherInfo> { @@ -138,6 +152,7 @@ public class WeatherUpdateService extends Service { if (D) Log.d(TAG, "Starting weather update task"); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); mContext = WeatherUpdateService.this; } @@ -151,12 +166,32 @@ public class WeatherUpdateService extends Service { LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); Location location = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); if (D) Log.v(TAG, "Current location is " + location); + + // If lastKnownLocation is not present (because none of the apps in the + // device has requested the current location to the system yet) or outdated, + // then try to get the current location use the provider that best matches the criteria. + boolean needsUpdate = location == null; + if (location != null) { + long delta = System.currentTimeMillis() - location.getTime(); + needsUpdate = delta > OUTDATED_LOCATION_THRESHOLD_MILLIS; + } + if (needsUpdate) { + if (D) Log.d(TAG, "Getting best location provider"); + String locationProvider = lm.getBestProvider(sLocationCriteria, true); + if (TextUtils.isEmpty(locationProvider)) { + Log.e(TAG, "No available location providers matching criteria."); + } else { + WeatherLocationListener.registerIfNeeded(mContext, locationProvider); + } + } + return location; } @Override protected WeatherInfo doInBackground(Void... params) { - WeatherProvider provider = new YahooWeatherProvider(mContext); + WeatherProvider provider = Preferences.weatherProvider(mContext); + boolean metric = Preferences.useMetricUnits(mContext); String customLocationId = null, customLocationName = null; if (Preferences.useCustomWeatherLocation(mContext)) { @@ -165,31 +200,22 @@ public class WeatherUpdateService extends Service { } if (customLocationId != null) { - return provider.getWeatherInfo(customLocationId, customLocationName); + return provider.getWeatherInfo(customLocationId, customLocationName, metric); } Location location = getCurrentLocation(); if (location != null) { - WeatherInfo info = provider.getWeatherInfo(location); + WeatherInfo info = provider.getWeatherInfo(location, metric); if (info != null) { return info; } } + // work with cached location from last request for now + // a listener to update it is already scheduled if possible WeatherInfo cachedInfo = Preferences.getCachedWeatherInfo(mContext); if (cachedInfo != null) { - return provider.getWeatherInfo(cachedInfo.getId(), cachedInfo.getCity()); - } - // If lastKnownLocation is not present because none of the apps in the - // device has requested the current location to the system yet, then try to - // get the current location use the provider that best matches the criteria. - if (D) Log.d(TAG, "Getting best location provider"); - LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); - String locationProvider = lm.getBestProvider(sLocationCriteria, true); - if (TextUtils.isEmpty(locationProvider)) { - Log.e(TAG, "No available location providers matching criteria."); - } else { - WeatherLocationListener.registerIfNeeded(mContext, locationProvider); + return provider.getWeatherInfo(cachedInfo.getId(), cachedInfo.getCity(), metric); } return null; @@ -224,6 +250,10 @@ public class WeatherUpdateService extends Service { scheduleUpdate(mContext, interval, false); } + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, result == null); + sendBroadcast(finishedIntent); + if (D) Log.d(TAG, "RELEASING WAKELOCK"); mWakeLock.release(); stopSelf(); @@ -232,6 +262,7 @@ public class WeatherUpdateService extends Service { private static class WeatherLocationListener implements LocationListener { private Context mContext; + private PendingIntent mTimeoutIntent; private static WeatherLocationListener sInstance = null; static void registerIfNeeded(Context context, String provider) { @@ -248,35 +279,79 @@ public class WeatherUpdateService extends Service { // Check whether the provider is supported. // NOTE!!! Actually only WeatherUpdateService class is calling this function // with the NETWORK_PROVIDER, so setting the instance is safe. We must - // change this if this call receive differents providers + // change this if this call receive different providers LocationProvider lp = locationManager.getProvider(provider); if (lp != null) { if (D) Log.d(TAG, "LocationManager - Requesting single update"); locationManager.requestSingleUpdate(provider, sInstance, appContext.getMainLooper()); + sInstance.setTimeoutAlarm(); } } } } + static void cancel(Context context) { + synchronized (WeatherLocationListener.class) { + if (sInstance != null) { + final Context appContext = context.getApplicationContext(); + final LocationManager locationManager = + (LocationManager) appContext.getSystemService(Context.LOCATION_SERVICE); + if (D) Log.d(TAG, "Aborting location request after timeout"); + locationManager.removeUpdates(sInstance); + sInstance.cancelTimeoutAlarm(); + sInstance = null; + } + } + } + private WeatherLocationListener(Context context) { super(); mContext = context; } + private void setTimeoutAlarm() { + Intent intent = new Intent(mContext, WeatherUpdateService.class); + intent.setAction(ACTION_CANCEL_LOCATION_UPDATE); + + mTimeoutIntent = PendingIntent.getService(mContext, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + long elapseTime = SystemClock.elapsedRealtime() + LOCATION_REQUEST_TIMEOUT; + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, elapseTime, mTimeoutIntent); + } + + private void cancelTimeoutAlarm() { + if (mTimeoutIntent != null) { + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + am.cancel(mTimeoutIntent); + mTimeoutIntent = null; + } + } + @Override public void onLocationChanged(Location location) { // Now, we have a location to use. Schedule a weather update right now. if (D) Log.d(TAG, "The location has changed, schedule an update "); synchronized (WeatherLocationListener.class) { WeatherUpdateService.scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); sInstance = null; } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // Not used + // Now, we have a location to use. Schedule a weather update right now. + if (D) Log.d(TAG, "The location service has become available, schedule an update "); + if (status == LocationProvider.AVAILABLE) { + synchronized (WeatherLocationListener.class) { + WeatherUpdateService.scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); + sInstance = null; + } + } } @Override @@ -298,9 +373,9 @@ public class WeatherUpdateService extends Service { am.set(AlarmManager.RTC_WAKEUP, due, getUpdateIntent(context, force)); } - public static void scheduleNextUpdate(Context context) { + public static void scheduleNextUpdate(Context context, boolean force) { long lastUpdate = Preferences.lastWeatherUpdateTimestamp(context); - if (lastUpdate == 0) { + if (lastUpdate == 0 || force) { scheduleUpdate(context, 0, true); } else { long interval = Preferences.weatherRefreshIntervalInMs(context); @@ -320,5 +395,6 @@ public class WeatherUpdateService extends Service { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.cancel(getUpdateIntent(context, true)); am.cancel(getUpdateIntent(context, false)); + WeatherLocationListener.cancel(context); } } diff --git a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java b/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java index dc714cb..5abe35d 100644 --- a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java +++ b/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java @@ -19,9 +19,12 @@ package com.cyanogenmod.lockclock.weather; import android.content.Context; import android.location.Location; import android.net.Uri; +import android.text.Html; +import android.text.TextUtils; import android.util.Log; -import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; import org.json.JSONArray; import org.json.JSONException; @@ -51,7 +54,7 @@ public class YahooWeatherProvider implements WeatherProvider { Uri.encode("select woeid, postal, admin1, admin2, admin3, " + "locality1, locality2, country from geo.places where " + "(placetype = 7 or placetype = 8 or placetype = 9 " + - "or placetype = 10 or placetype = 11) and text ="); + "or placetype = 10 or placetype = 11 or placetype = 20) and text ="); private static final String URL_PLACEFINDER = "http://query.yahooapis.com/v1/public/yql?format=json&q=" + Uri.encode("select woeid, city from geo.placefinder where gflags=\"R\" and text ="); @@ -67,9 +70,14 @@ public class YahooWeatherProvider implements WeatherProvider { } @Override + public int getNameResourceId() { + return R.string.weather_source_yahoo; + } + + @Override public List<LocationResult> getLocations(String input) { - String locale = mContext.getResources().getConfiguration().locale.getCountry(); - String params = "\"" + input + "\" and lang = \"" + locale + "\""; + String language = getLanguage(); + String params = "\"" + input + "\" and lang = \"" + language + "\""; String url = URL_LOCATION + Uri.encode(params); JSONObject jsonResults = fetchResults(url); if (jsonResults == null) { @@ -93,14 +101,14 @@ public class YahooWeatherProvider implements WeatherProvider { } return results; } catch (JSONException e) { - Log.e(TAG, "Received malformed places data", e); + Log.e(TAG, "Received malformed places data (input=" + input + ", lang=" + language + ")", e); } return null; } - public WeatherInfo getWeatherInfo(String id, String localizedCityName) { - String unit = Preferences.useMetricUnits(mContext) ? "c" : "f"; - String url = String.format(URL_WEATHER, id, unit); + @Override + public WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metric) { + String url = String.format(URL_WEATHER, id, metric ? "c" : "f"); String response = HttpRetriever.retrieve(url); if (response == null) { @@ -115,22 +123,31 @@ public class YahooWeatherProvider implements WeatherProvider { parser.parse(new InputSource(reader), handler); if (handler.isComplete()) { + // There are cases where the current condition is unknown, but the forecast + // is not - using the (inaccurate) forecast is probably better than showing + // the question mark + if (handler.conditionCode == 3200) { + handler.condition = handler.forecasts.get(0).condition; + handler.conditionCode = handler.forecasts.get(0).conditionCode; + } + WeatherInfo w = new WeatherInfo(mContext, id, - localizedCityName != null ? localizedCityName : handler.city, null, + localizedCityName != null ? localizedCityName : handler.city, handler.condition, handler.conditionCode, handler.temperature, - handler.forecasts.get(0).low, handler.forecasts.get(0).high, handler.temperatureUnit, handler.humidity, handler.windSpeed, - handler.windDirection, handler.speedUnit, + handler.windDirection, handler.speedUnit, handler.forecasts, System.currentTimeMillis()); Log.d(TAG, "Weather updated: " + w); return w; + } else { + Log.w(TAG, "Received incomplete weather XML (id=" + id + ")"); } } catch (ParserConfigurationException e) { Log.e(TAG, "Could not create XML parser", e); } catch (SAXException e) { - Log.e(TAG, "Could not parse weather XML", e); + Log.e(TAG, "Could not parse weather XML (id=" + id + ")", e); } catch (IOException e) { - Log.e(TAG, "Could not parse weather XML", e); + Log.e(TAG, "Could not parse weather XML (id=" + id + ")", e); } return null; @@ -144,12 +161,6 @@ public class YahooWeatherProvider implements WeatherProvider { String condition; ArrayList<DayForecast> forecasts = new ArrayList<DayForecast>(); - private static class DayForecast { - float low, high; - int conditionCode; - String condition; - } - @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { @@ -168,11 +179,11 @@ public class YahooWeatherProvider implements WeatherProvider { conditionCode = (int) stringToFloat(attributes.getValue("code"), -1); temperature = stringToFloat(attributes.getValue("temp"), Float.NaN); } else if (qName.equals("yweather:forecast")) { - DayForecast day = new DayForecast(); - day.low = stringToFloat(attributes.getValue("low"), Float.NaN); - day.high = stringToFloat(attributes.getValue("high"), Float.NaN); - day.condition = attributes.getValue("text"); - day.conditionCode = (int) stringToFloat(attributes.getValue("code"), -1); + DayForecast day = new DayForecast( + /* low */ stringToFloat(attributes.getValue("low"), Float.NaN), + /* high */ stringToFloat(attributes.getValue("high"), Float.NaN), + /* condition */ attributes.getValue("text"), + /* conditionCode */ (int) stringToFloat(attributes.getValue("code"), -1)); if (!Float.isNaN(day.low) && !Float.isNaN(day.high) && day.conditionCode >= 0) { forecasts.add(day); } @@ -194,10 +205,11 @@ public class YahooWeatherProvider implements WeatherProvider { } } - public WeatherInfo getWeatherInfo(Location location) { - String locale = mContext.getResources().getConfiguration().locale.getCountry(); - String params = String.format(Locale.US, "\"%f %f\" and lang=\"%s\"", - location.getLatitude(), location.getLongitude(), locale); + @Override + public WeatherInfo getWeatherInfo(Location location, boolean metric) { + String language = getLanguage(); + String params = String.format(Locale.US, "\"%f %f\" and locale=\"%s\"", + location.getLatitude(), location.getLongitude(), language); String url = URL_PLACEFINDER + Uri.encode(params); JSONObject results = fetchResults(url); if (results == null) { @@ -209,17 +221,24 @@ public class YahooWeatherProvider implements WeatherProvider { String woeid = result.getString("woeid"); String city = result.getString("city"); + if (city == null) { + city = result.getString("neighborhood"); + } + + // The city name in the placefinder result is HTML encoded :-( + if (city != null) { + city = Html.fromHtml(city).toString(); + } + Log.d(TAG, "Resolved location " + location + " to " + city + " (" + woeid + ")"); - WeatherInfo info = getWeatherInfo(woeid, city); + WeatherInfo info = getWeatherInfo(woeid, city, metric); if (info != null) { - // cache the result for potential reuse - // (the placefinder service API is rate limited) - Preferences.setCachedLocationId(mContext, woeid); return info; } } catch (JSONException e) { - Log.e(TAG, "Received malformed placefinder data", e); + Log.e(TAG, "Received malformed placefinder data (location=" + + location + ", lang=" + language + ")", e); } return null; @@ -243,6 +262,11 @@ public class YahooWeatherProvider implements WeatherProvider { } } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "JSON data " + place.toString() + " -> id=" + result.id + + ", city=" + result.city + ", country=" + result.countryId); + } + if (result.id == null || result.city == null || result.countryId == null) { return null; } @@ -264,9 +288,20 @@ public class YahooWeatherProvider implements WeatherProvider { JSONObject rootObject = new JSONObject(response); return rootObject.getJSONObject("query").getJSONObject("results"); } catch (JSONException e) { - Log.w(TAG, "Received malformed places data", e); + Log.w(TAG, "Received malformed places data (url=" + url + ")", e); } return null; } -}; + + private String getLanguage() { + Locale locale = mContext.getResources().getConfiguration().locale; + String country = locale.getCountry(); + String language = locale.getLanguage(); + + if (TextUtils.isEmpty(country)) { + return language; + } + return language + "-" + country; + } +} |