/* * Copyright (C) 2014-2016 VersoBit * * This file is part of Weather Doge. * * Weather Doge is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Weather Doge is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Weather Doge. If not, see <http://www.gnu.org/licenses/>. */ package com.versobit.weatherdoge; import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.DisplayMetrics; import android.util.TypedValue; import org.apache.commons.lang3.ArrayUtils; import java.util.Random; public final class WidgetProvider extends AppWidgetProvider { // Will only be called once (on widget startup) @Override public void onUpdate(Context ctx, AppWidgetManager appWidgetManager, int[] appWidgetIds) { resetAlarm(ctx); ctx.startService(new Intent(ctx, WidgetService.class) .setAction(WidgetService.ACTION_REFRESH_MULTIPLE) .putExtra(WidgetService.EXTRA_WIDGET_ID, appWidgetIds)); } @Override public void onEnabled(Context ctx) { resetAlarm(ctx); } @Override public void onDisabled(Context ctx) { AlarmManager am = (AlarmManager)ctx.getSystemService(Context.ALARM_SERVICE); am.cancel(PendingIntent.getService(ctx, 0, new Intent(ctx, WidgetService.class) .setAction(WidgetService.ACTION_REFRESH_ALL), 0)); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onAppWidgetOptionsChanged(Context ctx, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { super.onAppWidgetOptionsChanged(ctx, appWidgetManager, appWidgetId, newOptions); ctx.startService(new Intent(ctx, WidgetService.class) .setAction(WidgetService.ACTION_REFRESH_ONE) .putExtra(WidgetService.EXTRA_WIDGET_ID, appWidgetId)); } static void resetAlarm(Context ctx) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); long interval = Integer.parseInt(prefs.getString(OptionsActivity.PREF_WIDGET_REFRESH, "1800")) * 1000L; AlarmManager am = (AlarmManager)ctx.getSystemService(Context.ALARM_SERVICE); PendingIntent pIntent = PendingIntent.getService(ctx, 0, new Intent(ctx, WidgetService.class).setAction(WidgetService.ACTION_REFRESH_ALL), 0); am.cancel(pIntent); am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, interval, interval, pIntent); } static Bitmap[] getTextBitmaps(Context ctx, String temp, String description, String location, String lastUpdated) { Bitmap[] bitmaps = { null, null, null, null }; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); boolean useComicNeue = prefs.getBoolean(OptionsActivity.PREF_WIDGET_USE_COMIC_NEUE, false) || BuildConfig.FLAVOR.equals(BuildConfig.FLAVOR_FOSS); Resources res = ctx.getResources(); Typeface primaryFont = Typeface.createFromAsset(ctx.getAssets(), useComicNeue ? "ComicNeue-Regular.ttf" : "comic.ttf"); Typeface secondaryFont = Typeface.createFromAsset(ctx.getAssets(), "RobotoCondensed-Regular.ttf"); float shadowRadius = res.getDimension(R.dimen.widget_text_shadow_radius); // Odd results with fractional offsets float shadowXY = Math.round(res.getDimension(R.dimen.widget_text_shadow_xy)); // Better to have more padding than not enough int shadowPadX = (int)Math.ceil(res.getDimension(R.dimen.widget_text_shadow_padding_width)); int shadowPadY = (int)Math.ceil(res.getDimension(R.dimen.widget_text_shadow_padding_height)); // Configure text painter Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG); textPaint.setTypeface(primaryFont); textPaint.setColor(Color.WHITE); textPaint.setStyle(Paint.Style.FILL); textPaint.setTextSize(res.getDimension(R.dimen.widget_temp_font_size)); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setShadowLayer(shadowRadius, shadowXY, shadowXY, Color.BLACK); // Rect textBounds = new Rect(); textPaint.getTextBounds(temp, 0, temp.length(), textBounds); bitmaps[0] = Bitmap.createBitmap(textBounds.width() + shadowPadX, textBounds.height() + shadowPadY, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(bitmaps[0]); c.drawText(temp, textBounds.width() / 2f, textBounds.height(), textPaint); textPaint.setTextSize(res.getDimension(R.dimen.widget_desc_font_size)); textBounds = new Rect(); textPaint.getTextBounds(description + "g", 0, description.length() + 1, textBounds); Rect b2 = new Rect(); textPaint.getTextBounds("a", 0, 1, b2); bitmaps[1] = Bitmap.createBitmap(textBounds.width() + shadowPadX, textBounds.height() + shadowPadY, Bitmap.Config.ARGB_8888); c = new Canvas(bitmaps[1]); c.drawText(description, textBounds.width() / 2f, (textBounds.height() + b2.height()) / 2f, textPaint); textPaint.setTextSize(res.getDimension(R.dimen.widget_bottom_bar_font_size)); textPaint.setTypeface(secondaryFont); textPaint.setShadowLayer(0, 0, 0, Color.BLACK); textBounds = new Rect(); textPaint.getTextBounds(location + "g", 0, location.length() + 1, textBounds); b2 = new Rect(); textPaint.getTextBounds("a", 0, 1, b2); bitmaps[2] = Bitmap.createBitmap(textBounds.width(), textBounds.height(), Bitmap.Config.ARGB_8888); c = new Canvas(bitmaps[2]); c.drawText(location, c.getWidth() / 2f, (c.getHeight() + b2.height()) / 2f, textPaint); textBounds = new Rect(); textPaint.getTextBounds(lastUpdated + "g", 0, lastUpdated.length() + 1, textBounds); bitmaps[3] = Bitmap.createBitmap(textBounds.width(), textBounds.height(), Bitmap.Config.ARGB_8888); c = new Canvas(bitmaps[3]); c.drawText(lastUpdated, c.getWidth() / 2f, (c.getHeight() + b2.height()) / 2f + 1, textPaint); return bitmaps; } static Bitmap getStatusBitmap(Context ctx, String status) { Typeface roboto = Typeface.createFromAsset(ctx.getAssets(), "RobotoCondensed-Regular.ttf"); Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG); textPaint.setTypeface(roboto); textPaint.setColor(Color.WHITE); textPaint.setStyle(Paint.Style.FILL); textPaint.setTextAlign(Paint.Align.CENTER); textPaint.setTextSize(ctx.getResources().getDimension(R.dimen.widget_bottom_bar_font_size)); Rect textBounds = new Rect(); textPaint.getTextBounds(status + "g", 0, status.length() + 1, textBounds); Rect baselineBounds = new Rect(); textPaint.getTextBounds("a", 0, 1, baselineBounds); Bitmap loading = Bitmap.createBitmap(textBounds.width(), textBounds.height(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(loading); canvas.drawText(status, canvas.getWidth() / 2f, (canvas.getHeight() + baselineBounds.height()) / 2f, textPaint); return loading; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private static float[] getWidgetSize(DisplayMetrics metrics, Bundle options) { return new float[] { TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH), metrics), TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT), metrics) }; } // Updates the sky bitmap for a single app widget instance @TargetApi(Build.VERSION_CODES.JELLY_BEAN) static Bitmap getSkyBitmap(Context ctx, Bundle options, int skyId) { Resources res = ctx.getResources(); Bitmap originalSky = BitmapFactory.decodeResource(res, skyId); // Obtain (approximate?) size of widget (and by extension the image view) float[] widgetSize = getWidgetSize(res.getDisplayMetrics(), options); float viewW = widgetSize[0], viewH = widgetSize[1]; // Obtain size of sky bitmap float bmpW = originalSky.getWidth(), bmpH = originalSky.getHeight(); // For some reason the calculated view height is too small by about a pixel or so. Let's // compensate for that and while we're at it increase the height by the radius of the // rounded edges to hide the edges behind the bottom bar float bottomBarHeight = res.getDimension(R.dimen.widget_bottom_bar_height); float radius = res.getDimension(R.dimen.widget_corner_radius); float extra = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, res.getDisplayMetrics()); viewH += extra + radius - bottomBarHeight; // Implement ImageView's CENTER_CROP scale type Matrix skyMatrix = new Matrix(); { float scale; float dx = 0, dy = 0; if (bmpW * viewH > viewW * bmpH) { scale = viewH / bmpH; dx = (viewW - bmpW * scale) * 0.5f; } else { scale = viewW / bmpW; dy = (viewH - bmpH * scale) * 0.5f; } skyMatrix.setScale(scale, scale); skyMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); } // Create a new bitmap/canvas pair at the size we need and draw on it Bitmap scaledSky = Bitmap.createBitmap((int) viewW, (int) viewH, Bitmap.Config.ARGB_8888); Canvas scaledCanvas = new Canvas(scaledSky); scaledCanvas.drawBitmap(originalSky, skyMatrix, new Paint()); originalSky.recycle(); // Rounded corner time! We need to round the top two corners. BitmapShader shader = new BitmapShader(scaledSky, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); cornerPaint.setShader(shader); Bitmap roundedSky = Bitmap.createBitmap(scaledSky.getWidth(), scaledSky.getHeight(), Bitmap.Config.ARGB_8888); Canvas roundedCanvas = new Canvas(roundedSky); roundedCanvas.drawRoundRect(new RectF(0, 0, scaledSky.getWidth(), scaledSky.getHeight()), radius, radius, cornerPaint); scaledSky.recycle(); return roundedSky; } static Bitmap getWowLayer(Context ctx, Bundle options, String image, int temp) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); boolean useComicNeue = prefs.getBoolean(OptionsActivity.PREF_WIDGET_USE_COMIC_NEUE, false) || BuildConfig.FLAVOR.equals(BuildConfig.FLAVOR_FOSS); Resources res = ctx.getResources(); // Find the size in pixels of the widget float[] widgetSize = getWidgetSize(res.getDisplayMetrics(), options); // Subtract out the bottom bar so now we have the rough size of the wowlayer widgetSize[1] -= res.getDimension(R.dimen.widget_bottom_bar_height); // Text shadow radius for wow text float shadowRadius = res.getDimension(R.dimen.widget_wowlayer_shadow_radius); // Odd results with fractional offsets float shadowXY = Math.max(1, Math.round(res.getDimension(R.dimen.widget_wowlayer_shadow_xy))); // Create a bitmap + canvas the size of the wowlayer to draw wows on Bitmap bitmap = Bitmap.createBitmap((int) widgetSize[0], (int) widgetSize[1], Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); // Setup the initial text painter Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG); textPaint.setTypeface(Typeface.createFromAsset(ctx.getAssets(), useComicNeue ? "ComicNeue-Regular.ttf" : "comic.ttf")); textPaint.setStyle(Paint.Style.FILL); textPaint.setTextAlign(Paint.Align.LEFT); textPaint.setTextSize(res.getDimension(useComicNeue ? R.dimen.widget_wowlayer_font_size_neue : R.dimen.widget_wowlayer_font_size)); textPaint.setShadowLayer(shadowRadius, shadowXY, shadowXY, Color.BLACK); // Initialize the random number generator Random r = new Random(); // Find the colors, and text to use for our dogeisms int[] colors = res.getIntArray(R.array.wow_colors); String[] wows = res.getStringArray(R.array.wows); String[] dogefixes = res.getStringArray(R.array.dogefix); String[] weatherAdjs = ArrayUtils.addAll( res.getStringArray(WeatherDoge.getTempAdjectives(temp)), res.getStringArray(WeatherDoge.getBgAdjectives(image)) ); // Calculate how many dogeisms to display for a given density independent widget area double area = (widgetSize[0] / res.getDisplayMetrics().density) * (widgetSize[1] / res.getDisplayMetrics().density); int total = (int)Math.max(1, Math.round(0.000066156726853611d * area + 1.1833074350128d)); // Store our drawn rectangles so we can avoid text overlaps Rect[] drawnRects = new Rect[total + 1]; // Prevent text from drawing underneath the temperature display at the bottom left drawnRects[total] = new Rect(0, (int)(widgetSize[1] - res.getDimension(R.dimen.widget_wowlayer_rect_top)), (int)res.getDimension(R.dimen.widget_wowlayer_rect_right), (int)widgetSize[1]); // Store dogeisms used to avoid duplicates String[] drawnText = new String[total]; // Loop over all the dogeisms we're going to create for(int i = 0; i < total; i++) { Rect textBounds = new Rect(); // The rectangle which represents the text's actual position on the canvas Rect realRect = new Rect(); textPaint.setColor(colors[r.nextInt(colors.length)]); // The text to draw drawnText[i] = getUniqueDogeism(drawnText, r, wows, dogefixes, weatherAdjs); // Find the bounding rectangle of the text textPaint.getTextBounds(drawnText[i], 0, drawnText[i].length(), textBounds); // Prevent the text from going offscreen int xMax = (int)widgetSize[0] - textBounds.width(); int yMax = (int)widgetSize[1] - textBounds.height(); do { // Create the real rectangle using a random origin realRect.left = r.nextInt(xMax); realRect.right = realRect.left + textBounds.width(); realRect.bottom = r.nextInt(yMax); realRect.top = realRect.bottom - textBounds.height(); // Continue to loop until we find a valid in-bounds rectangle. // Since the drawText origin is the bottom left we need to make certain that // our text will not clip off the top edge of the widget. // We also need to check if this rectangle intersects any previously drawn // text rectangles. We don't want text-on-text. } while(realRect.bottom < textBounds.height() || intersects(realRect, drawnRects)); // realRect is valid. Draw it out. canvas.drawText(drawnText[i], realRect.left, realRect.bottom, textPaint); // Store it for intersection checks drawnRects[i] = realRect; } return bitmap; } private static String getUniqueDogeism(String[] existing, Random r, String[] wows, String[] dogefixes, String[] weatherAdjs) { String ism = null; while(ism == null) { ism = WeatherDoge.getDogeism(r, wows, dogefixes, weatherAdjs); for(String s : existing) { if(s == null) { continue; } if(ism.equals(s)) { ism = null; break; } } } return ism; } // Searches a Rect array for an intersection with a given Rect private static boolean intersects(Rect needle, Rect[] haystack) { for(Rect r : haystack) { if(r != null && Rect.intersects(r, needle)) { return true; } } return false; } }