/* * Copyright 2014 Sebastiano Poggi and Francesco Pontillo * * 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 net.frakbot.FWeather.updater; import android.annotation.TargetApi; import android.app.IntentService; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.Settings; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.View; import android.widget.RemoteViews; import android.widget.Toast; import net.frakbot.FWeather.R; import net.frakbot.FWeather.activity.SettingsActivity; import net.frakbot.FWeather.updater.weather.model.WeatherData; import net.frakbot.FWeather.util.*; import net.frakbot.global.Const; import net.frakbot.util.log.FLog; import java.io.IOException; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Updater service for the widgets. * TODO: deregister all location providers when no widgets are available * * @author Sebastiano Poggi, Francesco Pontillo */ public class UpdaterService extends IntentService { public static final String TAG = UpdaterService.class.getSimpleName(); private WidgetHelper mWidgetHelper; public static final String EXTRA_USER_FORCE_UPDATE = "the_motherfocker_wants_us_to_do_stuff"; public static final String EXTRA_SILENT_FORCE_UPDATE = "a_ninja_is_making_me_do_it"; public static final String EXTRA_WIDGET_IDS = "widget_ids"; private static final Pattern REGEX_LANGCODE_SIMPLE = Pattern.compile("[a-z]{2}"); private static final Pattern REGEX_LANGCODE_COUNTRY = Pattern.compile("([a-z]{2})\\-([A-Z]{2})"); private Handler mHandler; public UpdaterService() { super(UpdaterService.class.getSimpleName()); } @Override public void onCreate() { super.onCreate(); FLog.d(this, TAG, "onCreate"); FLog.i(this, TAG, "Initializing the UpdaterService"); mWidgetHelper = new WidgetHelper(this); mHandler = new Handler(); // Initialize the amazing LocationHelper // (the method is idempotent) LocationHelper.init(this); } @Override protected void onHandleIntent(Intent intent) { FLog.d(this, TAG, "onHandleIntent"); // Deregister the connection listener, if any ConnectionHelper.unregisterConnectivityListener(getApplicationContext()); // First thing, recheck the log filtering levels FLog.recheckLogLevels(); int[] appWidgetIds = intent.getIntArrayExtra(EXTRA_WIDGET_IDS); if (appWidgetIds == null || appWidgetIds.length == 0) { FLog.d(this, TAG, "Intent with no widgets ID received, ignoring\n\t> " + intent); return; } boolean forced = intent.getBooleanExtra(EXTRA_USER_FORCE_UPDATE, false); if (forced) { FLog.i(this, TAG, "User has requested a forced update"); // Show a toast message mHandler.post(new Runnable() { @Override public void run() { // We need this because the IntentService thread is too fast and dies too soon, // resulting in the toast being on screen for an unpercievable time WidgetHelper.makeToast(UpdaterService.this, R.string.toast_force_update, Toast.LENGTH_LONG) .show(); } }); } FLog.i(this, TAG, "Starting widgets update"); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); assert appWidgetManager != null; // Get the latest weather info (new or cached) WeatherData weather; try { weather = WeatherHelper.getWeather(this, forced); } catch (LocationHelper.LocationNotReadyYetException justWaitException) { // If the location is not ready yet, leave the View unchanged FLog.d(this, TAG, "The LocationHelper is not ready yet, the updater will be called again " + "when a location is available."); weather = new WeatherData(); weather.conditionCode = WeatherData.INVALID_CONDITION; } catch (IOException e) { // Caught if there are connection issues // Get the latest cached weather information FLog.e(this, TAG, "Error while fetching the weather, using a cached value", e); weather = WeatherHelper.getLatestWeather(); // Register a connection listener FLog.d(this, TAG, "Registering a connection listener"); ConnectionHelper.registerConnectivityListener(getApplicationContext()); } Locale defaultLocale = null, selectedLocale; if ((selectedLocale = getUserSelectedLocale(this)) != null) { defaultLocale = switchLocale(this, selectedLocale); } // Perform this loop procedure for each App Widget that belongs to this provider for (int appWidgetId : appWidgetIds) { FLog.i(this, TAG, "Updating the widget views for widget #" + appWidgetId); // Get the widget layout and update it RemoteViews views = new RemoteViews(getPackageName(), getWidgetLayout(appWidgetManager, appWidgetId)); updateViews(views, weather, appWidgetIds); setupViewsForKeyguard(views, appWidgetManager, appWidgetId); // Tell the AppWidgetManager to perform an update on the current app widget appWidgetManager.updateAppWidget(appWidgetId, views); } // If we switched the locale, let's restore the default one if (selectedLocale != null) { switchLocale(this, defaultLocale); } // Reschedule the alarm AlarmHelper.rescheduleAlarm(this); FLog.i(this, TAG, "All widgets updated successfully"); } /** * Change the Locale used for further (even implicit) calls to <code>getResources()</code> . * * @param selectedLocale The new locale to use * @param context The current context * @return Returns the Locale used before the switch. It should be restored after use. */ public static Locale switchLocale(Context context, Locale selectedLocale) { FLog.v(TAG, "Switching locale to " + selectedLocale.toString()); Resources standardResources = context.getResources(); AssetManager assets = standardResources.getAssets(); DisplayMetrics metrics = standardResources.getDisplayMetrics(); Configuration config = new Configuration(standardResources.getConfiguration()); // Backup the current default locale, in order to restore it after the update Locale currentLocale = config.locale; config.locale = selectedLocale; // no need to assign this to a variable: the app will use these resources until they are changed again new Resources(assets, metrics, config); return currentLocale; } /** * Check the current default language against the language selected by the user in the preferences screen * * @param context The current context * @return Returns the Locale related to the language choosen by the user or <code>null</code> if the user * didn't choose any other locale */ public static Locale getUserSelectedLocale(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final String LANG_AUTO = context.getResources().getStringArray(R.array.pref_key_ui_override_language_values)[0]; final String preferenceValue = prefs.getString(Const.Preferences.UI_OVERRIDE_LANGUAGE, LANG_AUTO); // Extract the target language (and, if present, country) from the preference String targetLanguage = null; String targetCountry = null; final Matcher simpleLangFormat = REGEX_LANGCODE_SIMPLE.matcher(preferenceValue); final Matcher countryLangFormat = REGEX_LANGCODE_COUNTRY.matcher(preferenceValue); if (simpleLangFormat.matches()) { // The saved preferences string is a ISO1 language code targetLanguage = preferenceValue; } else if (countryLangFormat.matches()) { // We expect a format like "en-US", where 'en' is the ISO1 language code // and 'US' is the ISO country variant targetLanguage = countryLangFormat.group(1); targetCountry = countryLangFormat.group(2); } else { FLog.w(TAG, "Invalid locale detected in the preferences: " + preferenceValue + ". Resetting to AUTO"); prefs.edit() .putString(Const.Preferences.UI_OVERRIDE_LANGUAGE, LANG_AUTO) .commit(); } // Retrieve the current locale (or use the default locale) Configuration currentConfig = new Configuration(context.getResources().getConfiguration()); Locale currentLocale = Locale.getDefault(); if (currentConfig.locale != null) { currentLocale = currentConfig.locale; } // We only need to change the locale if we actually have a target locale, // or if the target locale is different from the current (or it's the same // but has a different country code), or if we are going to use the system // locale setting (locale switching is temporary/atomic for each run) if (targetLanguage == null || (currentLocale.getLanguage().equals(targetLanguage) && (targetCountry == null || currentLocale.getCountry().equals(targetCountry))) || (targetLanguage.equals(LANG_AUTO))) { // No need to change locale return null; } return TextUtils.isEmpty(targetCountry) ? new Locale(targetLanguage) : new Locale(targetLanguage, targetCountry); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private int getWidgetLayout(AppWidgetManager appWidgetManager, int widgetId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { Bundle myOptions = appWidgetManager.getAppWidgetOptions(widgetId); // Get the value of OPTION_APPWIDGET_HOST_CATEGORY int maxHeight = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, -1); // If the value is WIDGET_CATEGORY_KEYGUARD, it's a lockscreen widget if (maxHeight < 200) { return R.layout.fweather_small; } else if (maxHeight < 300) { return R.layout.fweather_medium; } else { return R.layout.fweather_large; } } else { return R.layout.fweather_large; } } /** * Updates the widget's views. * * @param views The RemoteViews to use * @param weather The weather to update with */ private void updateViews(RemoteViews views, WeatherData weather, int[] widgetIds) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final boolean darkMode = prefs.getBoolean(getString(R.string.pref_key_ui_darkmode), false); // Determine the main text color for the widget int textColor; if (!darkMode) { textColor = getResources().getColor(R.color.text_widget_main_color); } else { textColor = getResources().getColor(R.color.text_widget_main_color_dark); } // Show/hide elements, and update them only if needed views.setTextViewText(R.id.txt_weather, mWidgetHelper.getWeatherString(weather, darkMode)); views.setTextColor(R.id.txt_weather, textColor); int bgColorPrefValue = getWidgetBgColorPrefValue(prefs); views.setInt(R.id.content, "setBackgroundColor", mWidgetHelper.getWidgetBGColor(bgColorPrefValue, darkMode)); if (prefs.getBoolean(getString(R.string.pref_key_ui_toggle_temperature_info), true)) { views.setViewVisibility(R.id.txt_temp, View.VISIBLE); views.setTextViewText(R.id.txt_temp, mWidgetHelper.getTempString(weather, darkMode)); views.setTextColor(R.id.txt_temp, textColor); } else { views.setViewVisibility(R.id.txt_temp, View.GONE); } if (prefs.getBoolean(getString(R.string.pref_key_ui_toggle_weather_icon), true)) { views.setViewVisibility(R.id.img_weathericon, View.VISIBLE); views.setImageViewResource(R.id.img_weathericon, mWidgetHelper.getWeatherImageId(weather, darkMode)); } else { views.setViewVisibility(R.id.img_weathericon, View.INVISIBLE); } if (prefs.getBoolean(getString(R.string.pref_key_ui_toggle_buttons), true)) { views.setViewVisibility(R.id.btn_settings, View.VISIBLE); views.setViewVisibility(R.id.btn_refresh, View.VISIBLE); views.setViewVisibility(R.id.btn_share, View.VISIBLE); views.setImageViewResource(R.id.btn_settings, darkMode ? R.drawable.ic_action_settings_dark : R.drawable.ic_action_settings); views.setImageViewResource(R.id.btn_refresh, darkMode ? R.drawable.ic_action_refresh_dark : R.drawable.ic_action_refresh); views.setImageViewResource(R.id.btn_share, darkMode ? R.drawable.ic_action_share_dark : R.drawable.ic_action_share); } else { views.setViewVisibility(R.id.btn_settings, View.GONE); views.setViewVisibility(R.id.btn_refresh, View.GONE); views.setViewVisibility(R.id.btn_share, View.GONE); } // Initalize OnClick listeners Intent i = new Intent(this, SettingsActivity.class); views.setOnClickPendingIntent(R.id.btn_settings, PendingIntent.getActivity(this, 0, i, 0)); i = new Intent(this, UpdaterService.class); i.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); i.putExtra(UpdaterService.EXTRA_WIDGET_IDS, widgetIds); i.putExtra(UpdaterService.EXTRA_USER_FORCE_UPDATE, true); views.setOnClickPendingIntent(R.id.btn_refresh, PendingIntent.getService(this, 0, i, 0)); // If the user hasn't enabled location settings and there's no information available, // they can tap the widget to open the system Location Settings activity if (weather != null && weather.conditionCode == WeatherData.WEATHER_ID_ERR_NO_LOCATION) { // The pending intent (Magnum PI, ha!) for the main TextViews PendingIntent enableLocationPendingIntent = PendingIntent.getActivity(this, 0, new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0); views.setOnClickPendingIntent(R.id.txt_weather, enableLocationPendingIntent); views.setOnClickPendingIntent(R.id.txt_temp, enableLocationPendingIntent); } // Create and set the PendingIntent for the share action if (weather != null && weather.conditionCode >= 0) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_TEXT, mWidgetHelper.getShareString(weather)); PendingIntent sharePendingIntent = PendingIntent.getActivity(this, 1, shareIntent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.btn_share, sharePendingIntent); } else { views.setViewVisibility(R.id.btn_share, View.GONE); } } /** * Gets the widget BG color opacity preference value, handling * any format errors that could arise. * * @param prefs The SharedPreferences to retrieve the value from * @return Returns the preference value, or a default value of 0 if there is * any issue with the preference value retrieval. */ private int getWidgetBgColorPrefValue(SharedPreferences prefs) { try { return Integer.parseInt(prefs.getString(getString(R.string.pref_key_ui_bgopacity), "%NOVAL%")); } catch (NumberFormatException e) { FLog.w(TAG, "Invalid preference value for UI BG opacity, defaulting to 0", e); return 0; } } /** * Sets up the widget views to adapt to it being on the lockscreen. * This method doesn't do anything on Android 4.1.x and earlier, since * there were no lockscreen widgets. * * @param views The widget RemoteViews * @param appWidgetManager The widget manager used to detect whether the widget * is on the lockscreen * @param widgetId The ID of the widget */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void setupViewsForKeyguard(RemoteViews views, AppWidgetManager appWidgetManager, int widgetId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Bundle myOptions = appWidgetManager.getAppWidgetOptions(widgetId); final int category = myOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN); if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) { FLog.v(TAG, "Hiding the refresh button: widget " + widgetId + " is on the keyguard"); views.setViewVisibility(R.id.btn_refresh, View.GONE); } } } }