/*
* Copyright (C) 2014-2017 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.app.Activity;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
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.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.plattysoft.leonids.ParticleSystem;
import org.apache.commons.lang3.ArrayUtils;
import java.text.DecimalFormat;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
final public class MainActivity extends Activity implements LocationReceiver,
ActivityCompat.OnRequestPermissionsResultCallback {
private static final long WOW_INTERVAL = 2300;
private static final String TAG = MainActivity.class.getSimpleName();
private static final int REQUEST_LOCATION_PERMISSION = 410;
private boolean forceMetric = false;
private String forceLocation = "";
private WeatherUtil.Source weatherSource = WeatherUtil.Source.OPEN_WEATHER_MAP;
private boolean useNeue = false;
private float shadowR = 1f;
private float shadowX = 3f;
private float shadowY = 3f;
private boolean shadowAdjs = false;
private boolean textOnTop = false;
private boolean enableParticles = true;
private int lastVersion = 0;
private ImageView suchBg;
private RelativeLayout suchOverlay;
private RelativeLayout suchTopOverlay;
private LinearLayout suchInfoGroup;
private ImageView suchDoge;
private TextView suchStatus;
private RelativeLayout suchTempGroup;
private TextView suchNegative;
private TextView suchTemp;
private TextView suchDegree;
private TextView suchLocation;
private FloatingActionsMenu suchMenu;
private LocationApi wowApi;
private Location whereIsDoge;
private Typeface wowComicSans;
private AlertDialog errorDialog;
private AlertDialog rationaleDialog;
private Snackbar locationSnackbar;
private ParticleSystem particleSystem;
private boolean isEmitting = false;
private double currentTemp;
private boolean currentlyMetric;
private String currentLocation;
private Uri currentLink;
private String[] dogefixes;
private String[] wows;
private String[] weatherAdjectives;
private int[] colors;
private Timer overlayTimer = new Timer("OverlayTimer", true);
private OverlayTimerTask overlayTask;
private WowText newWowText;
private Queue<WowText> overlays = new ArrayDeque<>(4);
private int currentBackgroundId = Integer.MIN_VALUE;
private int currentDogeId = R.drawable.doge_01d;
private boolean currentlyAnim = false;
private static final class WowText {
private RelativeLayout.LayoutParams params;
private TextView view;
private WowText(RelativeLayout.LayoutParams params, TextView view) {
this.params = params;
this.view = view;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadOptions();
dogefixes = getResources().getStringArray(R.array.dogefix);
wows = getResources().getStringArray(R.array.wows);
colors = getResources().getIntArray(R.array.wow_colors);
suchBg = (ImageView)findViewById(R.id.main_suchbg);
suchOverlay = (RelativeLayout)findViewById(R.id.main_suchoverlay);
suchTopOverlay = (RelativeLayout)findViewById(R.id.main_suchtopoverlay);
suchInfoGroup = (LinearLayout)findViewById(R.id.main_suchinfogroup);
suchInfoGroup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentLink == null) {
return;
}
Intent i = new Intent(Intent.ACTION_VIEW, currentLink);
WeatherDoge.applyChromeCustomTab(MainActivity.this, i);
startActivity(i);
}
});
suchDoge = (ImageView)findViewById(R.id.main_suchdoge);
suchDoge.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!forceLocation.isEmpty()) {
new GetWeather().execute();
} else if(wowApi != null && wowApi.isConnected()) {
requestLocation();
}
}
});
suchStatus = (TextView)findViewById(R.id.main_suchstatus);
suchTempGroup = (RelativeLayout)findViewById(R.id.main_suchtempgroup);
suchNegative = (TextView)findViewById(R.id.main_suchnegative);
suchTemp = (TextView)findViewById(R.id.main_suchtemp);
suchDegree = (TextView)findViewById(R.id.main_suchdegree);
suchLocation = (TextView)findViewById(R.id.main_suchlocation);
suchMenu = (FloatingActionsMenu)findViewById(R.id.main_fam);
findViewById(R.id.man_fab_share).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("text/plain");
if(weatherAdjectives == null) {
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name));
i.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text).split("\n\n")[1]);
} else {
String unit = (char)0x00b0 + "C";
double tempTemp = currentTemp; // temporary temperature...
if(UnitLocale.getDefault() == UnitLocale.IMPERIAL && !forceMetric) {
tempTemp = tempTemp * 1.8d + 32d; // F
unit = (char)0x00b0 + "F";
}
String temp = String.valueOf(Math.round(tempTemp)) + ' ' + unit;
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_title, temp, currentLocation));
i.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text, WeatherDoge.getDogeism(wows, dogefixes, weatherAdjectives), temp, currentLocation));
}
suchMenu.collapse();
startActivity(Intent.createChooser(i, getString(R.string.action_share)));
}
});
findViewById(R.id.man_fab_options).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
suchMenu.collapse();
startActivity(new Intent(MainActivity.this, OptionsActivity.class));
}
});
updateFont();
updateShadow();
if(!forceLocation.isEmpty()) {
new GetWeather().execute();
} else if(LocationApi.isAvailable(this)) {
wowApi = new LocationApi(this, this);
}
new SetBackgroundTask(R.drawable.sky_01d).execute();
if(BuildConfig.VERSION_CODE > lastVersion) {
lastVersion = BuildConfig.VERSION_CODE;
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().putInt(OptionsActivity.PREF_INTERNAL_LAST_VERSION, lastVersion).apply();
}
}
private void loadOptions() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
forceMetric = sp.getBoolean(OptionsActivity.PREF_FORCE_METRIC, false);
forceLocation = sp.getString(OptionsActivity.PREF_FORCE_LOCATION, "");
weatherSource = WeatherUtil.Source.fromKey(sp.getString(OptionsActivity.PREF_WEATHER_SOURCE,
WeatherUtil.Source.OPEN_WEATHER_MAP.getKey()));
useNeue = sp.getBoolean(OptionsActivity.PREF_APP_USE_COMIC_NEUE, false);
shadowR = sp.getFloat(OptionsActivity.PREF_APP_DROP_SHADOW + "_radius", 1f);
shadowX = sp.getFloat(OptionsActivity.PREF_APP_DROP_SHADOW + "_x", 3f);
shadowY = sp.getFloat(OptionsActivity.PREF_APP_DROP_SHADOW + "_y", 3f);
shadowAdjs = sp.getBoolean(OptionsActivity.PREF_APP_DROP_SHADOW + "_adjs", false);
textOnTop = sp.getBoolean(OptionsActivity.PREF_APP_TEXT_ON_TOP, false);
enableParticles = sp.getBoolean(OptionsActivity.PREF_APP_ENABLE_PARTICLES, true);
lastVersion = sp.getInt(OptionsActivity.PREF_INTERNAL_LAST_VERSION, lastVersion);
}
@Override
protected void onStart() {
super.onStart();
if(wowApi != null) {
requestLocation();
}
overlayTask = new OverlayTimerTask();
overlayTimer.schedule(overlayTask, 0, WOW_INTERVAL);
if (WeatherDoge.isSnowing(currentBackgroundId) && enableParticles) {
if (particleSystem != null) {
particleSystem.cancel();
}
particleSystem = newParticleSystem();
// Make it render as if it started 25 seconds ago
particleSystem.setStartTime(25000);
startParticleSystem();
}
}
@Override
protected void onStop() {
if(wowApi != null && wowApi.isConnected()) {
wowApi.disconnect();
}
overlayTask.cancel();
overlayTask = null;
if (particleSystem != null && isEmitting) {
particleSystem.cancel();
isEmitting = false;
}
super.onStop();
}
@Override
protected void onResume() {
super.onResume();
boolean oldNeue = useNeue;
float[] oldShadowFloats = { shadowR, shadowX, shadowY };
boolean oldShadowBool = shadowAdjs;
loadOptions();
if(useNeue != oldNeue) {
updateFont();
}
if(oldShadowFloats[0] != shadowR || oldShadowFloats[1] != shadowX || oldShadowFloats[2] != shadowY
|| oldShadowBool != shadowAdjs) {
updateShadow();
}
if(forceLocation.isEmpty()) {
if(wowApi == null) {
wowApi = new LocationApi(this, this);
}
if(!wowApi.isConnected() && !wowApi.isConnecting()) {
// FIXME: Double request after selecting cancel on the first permission dialog
requestLocation();
}
} else {
if(wowApi != null && wowApi.isConnected()) {
wowApi.disconnect();
}
new GetWeather().execute();
}
if (!enableParticles && isEmitting) {
if (particleSystem != null) {
particleSystem.cancel();
}
isEmitting = false;
} else if (WeatherDoge.isSnowing(currentBackgroundId) && enableParticles && !isEmitting) {
if (particleSystem != null) {
particleSystem.cancel();
}
particleSystem = newParticleSystem();
// Make it render as if it started 25 seconds ago
particleSystem.setStartTime(25000);
startParticleSystem();
isEmitting = true;
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Close the FAB if the user taps elsewhere
if (ev.getAction() == MotionEvent.ACTION_DOWN && suchMenu.isExpanded()) {
Rect outRect = new Rect();
suchMenu.getGlobalVisibleRect(outRect);
if (!outRect.contains((int)ev.getRawX(), (int)ev.getRawY())) {
suchMenu.collapse();
}
}
return super.dispatchTouchEvent(ev);
}
@Override
public void onBackPressed() {
// Close the FAB with the back button if it's open
if(suchMenu.isExpanded()) {
suchMenu.collapse();
return;
}
super.onBackPressed();
}
private void updateFont() {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)suchTemp.getLayoutParams();
if(useNeue || BuildConfig.FLAVOR.equals(BuildConfig.FLAVOR_FOSS)) {
wowComicSans = Typeface.createFromAsset(getAssets(), "ComicNeue-Regular.ttf");
// Horizontal and vertical centering for proper display
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
// This only works when going from Comic Sans -> Comic Neue, not the other way around
// Android doesn't redraw Comic Sans correctly, or something...
for(WowText wowText : overlays) {
wowText.view.setTypeface(wowComicSans);
}
}
else {
wowComicSans = Typeface.createFromAsset(getAssets(), "comic.ttf");
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, 0); // Disable vertical center
}
suchDegree.setTypeface(wowComicSans);
suchStatus.setTypeface(wowComicSans);
suchLocation.setTypeface(wowComicSans);
suchNegative.setTypeface(wowComicSans);
suchTemp.setTypeface(wowComicSans);
suchTemp.setLayoutParams(layoutParams);
}
private void updateShadow() {
suchStatus.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
suchNegative.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
suchTemp.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
suchDegree.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
suchLocation.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
for(WowText wowText : overlays) {
if(shadowAdjs) {
wowText.view.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
continue;
}
wowText.view.setShadowLayer(0, 0, 0, 0);
}
}
@Override
public void onConnected() {
requestLocation();
}
@Override
public void onLocation(Location location) {
whereIsDoge = location;
if(whereIsDoge == null) {
Log.e(TAG, "dunno where this shibe is");
return;
}
new GetWeather().execute(whereIsDoge);
}
private void requestLocation() {
requestLocation(false);
}
// Asynchronously request the current location
private void requestLocation(boolean skipRationale) {
// Check if we need to request the permission on >= Marshmallow
if (ContextCompat.checkSelfPermission(this, WeatherDoge.LOCATION_PERMISSION)
!= PackageManager.PERMISSION_GRANTED) {
// We need to request it
if (!skipRationale && ActivityCompat.shouldShowRequestPermissionRationale(this,
WeatherDoge.LOCATION_PERMISSION)) {
// Show the rationale dialog if we need to
showLocationRationaleDialog();
} else {
// Just request the permission
ActivityCompat.requestPermissions(this,
new String[] { WeatherDoge.LOCATION_PERMISSION },
REQUEST_LOCATION_PERMISSION);
}
} else {
// We already have it or this is < Marshmallow
doCheckedLocationRequest();
}
}
// Show the location rationale dialog which explains to the user why we need their location
private void showLocationRationaleDialog() {
if ((rationaleDialog != null && rationaleDialog.isShowing())
|| (locationSnackbar != null && locationSnackbar.isShown())) {
return;
}
AlertDialog.Builder adb = new AlertDialog.Builder(this)
.setMessage(R.string.location_rationale_text)
.setNegativeButton(R.string.location_rationale_negative, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Go directly to the constant location setting in the options
startActivity(new Intent(MainActivity.this, OptionsActivity.class)
.putExtra(OptionsActivity.EXTRA_SHORTCUT,
OptionsActivity.EXTRA_SHORTCUT_FORCE_LOCATION
));
}
})
.setPositiveButton(R.string.location_rationale_positive, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Request the permission again
ActivityCompat.requestPermissions(MainActivity.this,
new String[] { WeatherDoge.LOCATION_PERMISSION },
REQUEST_LOCATION_PERMISSION);
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
showLocationSnackbar();
}
});
// Prevent crash if MainActivity is finishing while attempting to display a new dialog
if(!isFinishing()) {
rationaleDialog = adb.show();
}
}
private void showLocationSnackbar() {
if(locationSnackbar != null && locationSnackbar.isShown()) {
return;
}
locationSnackbar = Snackbar.make(findViewById(R.id.main_suchlayout),
R.string.location_snackbar_text, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.location_snackbar_button, new View.OnClickListener() {
@Override
public void onClick(View v) {
requestLocation(true);
}
})
.setActionTextColor(ContextCompat.getColor(this, R.color.primary_dark));
locationSnackbar.show();
}
// Called after we're certain we have the location permission
private void doCheckedLocationRequest() {
// Make sure we're going to connect
if(!wowApi.isConnected() && !wowApi.isConnecting()) {
wowApi.connect();
}
onLocation(wowApi.getLocation());
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode != REQUEST_LOCATION_PERMISSION) {
// Not a request we're interested in
return;
}
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Remove the widget notification, if any
((NotificationManager)getSystemService(NOTIFICATION_SERVICE))
.cancel(WidgetService.PERMISSION_NOTIFICATION_ID);
// Refresh the widget
startService(new Intent(this, WidgetService.class).setAction(WidgetService.ACTION_REFRESH_ALL));
// Dismiss the snackbar, if any
if (locationSnackbar != null && locationSnackbar.isShown()) {
locationSnackbar.dismiss();
}
// Good to go, execute the actual location action
doCheckedLocationRequest();
} else {
showLocationSnackbar();
}
}
private ParticleSystem newParticleSystem() {
return new ParticleSystem(this, 200, R.drawable.snowflake, 20000)
.setSpeedByComponentsRange(0f, 0f, 0.05f, 0.1f)
.setScaleRange(0.2f, 1f)
.setParentViewGroup((ViewGroup)findViewById(R.id.main_snowframe));
}
private void startParticleSystem() {
isEmitting = true;
particleSystem.emitWithGravity(findViewById(R.id.snow_emitter), Gravity.TOP, 5);
}
private final class OverlayTimerTask extends TimerTask {
@Override
public void run() {
if(weatherAdjectives == null) {
return;
}
WowText wowText;
// Set up the RNG
Random r = new Random();
// Continue to loop until we come out the other side with a valid wowText
while(true) {
// Create the new view
wowText = new WowText(null, new TextView(MainActivity.this));
// 15sp is a magic padding number I've tested with
int padding = (int)Math.ceil(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, getResources().getDisplayMetrics()));
// Set up the view with the basics
wowText.view.setTypeface(wowComicSans);
if(shadowAdjs) {
wowText.view.setShadowLayer(shadowR, shadowX, shadowY, Color.BLACK);
}
// How big is the overlay layer?
int[] layoutDim = { suchOverlay.getWidth(), suchOverlay.getHeight() };
wowText.view.setText(getUniqueDogeism(r));
wowText.view.setTextColor(colors[r.nextInt(colors.length)]);
wowText.view.setTextSize(TypedValue.COMPLEX_UNIT_SP, r.nextInt(15) + 25);
// How big is this textview going to be?
wowText.view.measure(layoutDim[0], layoutDim[1]);
int[] textDim = { wowText.view.getMeasuredWidth(), wowText.view.getMeasuredHeight() };
// Set a fixed width and height
wowText.params = new RelativeLayout.LayoutParams(textDim[0], textDim[1]);
// Find the maximum left and top margins
int[] absPos = { layoutDim[0] - textDim[0], layoutDim[1] - textDim[1] };
// Can we fit this text on the screen?
if(absPos[0] < 0 || absPos[1] < 0) {
continue; // Can't fit with that dogeism, text size, and layout dimensions
}
wowText.params.leftMargin = absPos[0] == 0 ? 0 : r.nextInt(absPos[0]);
wowText.params.topMargin = absPos[1] == 0 ? 0 : r.nextInt(absPos[1]);
wowText.params.width += padding * 2; // left + right
wowText.params.height += padding * 2; // top + bottom
// Padding is subtracted from top/left margins so the measured values are still accurate
// We don't care if the shadow clips on the edge of the screen
// abs prevents possibly dangerous negative margins
wowText.params.leftMargin = Math.abs(wowText.params.leftMargin - padding);
wowText.params.topMargin = Math.abs(wowText.params.topMargin - padding);
wowText.view.setGravity(Gravity.CENTER); // Text is centered within the now padded view
if(checkWowTextConflict(wowText)) {
continue;
}
break;
}
newWowText = wowText;
runOnUiThread(overlayUiRunnable);
}
private String getUniqueDogeism(Random r) {
String ism = null;
while(ism == null) {
ism = WeatherDoge.getDogeism(r, wows, dogefixes, weatherAdjectives);
WowText head = overlays.peek();
for(WowText wow : overlays) {
if(head == wow && overlays.size() == 4) {
continue; // Continues on the inner loop
}
if(ism.contentEquals(wow.view.getText())) {
ism = null;
break;
}
}
}
return ism;
}
private boolean checkWowTextConflict(WowText needle) {
Rect needleRect = layoutParamsToRect(needle.params);
// head is the one to be removed
WowText head = overlays.peek();
for(WowText wow : overlays) {
if(head == wow && overlays.size() == 4) {
// We do not want to compare against head because
// it'll be removed
continue;
}
if(Rect.intersects(needleRect, layoutParamsToRect(wow.params))) {
return true;
}
}
return false;
}
private Rect layoutParamsToRect(RelativeLayout.LayoutParams params) {
return new Rect(params.leftMargin, params.topMargin,
params.leftMargin + params.width, params.topMargin + params.height);
}
private Runnable overlayUiRunnable = new Runnable() {
@Override
public void run() {
if(overlays.size() == 4) {
// If the view doesn't exist in the particular overlay it will not throw an exception
View v = overlays.remove().view;
suchOverlay.removeView(v);
suchTopOverlay.removeView(v);
}
if(textOnTop) {
suchTopOverlay.addView(newWowText.view, newWowText.params);
} else {
suchOverlay.addView(newWowText.view, newWowText.params);
}
overlays.add(newWowText);
}
};
}
private final class SetDogeTask extends AsyncTask<Void, Void, Animation> {
private final int resId;
private SetDogeTask(int resId) {
this.resId = resId;
}
@Override
protected void onPreExecute() {
if (currentDogeId == resId) {
cancel(true);
return;
}
currentDogeId = resId;
}
@Override
protected Animation doInBackground(Void... params) {
Animation zoomOut = AnimationUtils.loadAnimation(MainActivity.this, R.anim.dogezoom_out);
final Animation zoomIn = AnimationUtils.loadAnimation(MainActivity.this, R.anim.dogezoom_in);
zoomOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
suchDoge.setImageResource(resId);
suchDoge.startAnimation(zoomIn);
}
@Override
public void onAnimationRepeat(Animation animation) { }
});
return zoomOut;
}
@Override
protected void onPostExecute(Animation animation) {
suchDoge.startAnimation(animation);
}
}
private final class SetBackgroundTask extends AsyncTask<Void, Void, Bitmap> {
private final int resId;
private SetBackgroundTask(int resId) {
this.resId = resId;
}
@Override
protected void onPreExecute() {
if(currentBackgroundId == resId) {
cancel(true);
return;
}
currentBackgroundId = resId;
}
@Override
protected Bitmap doInBackground(Void... params) {
if(isCancelled()) {
return null;
}
// Manually resize/crop the sky background because god forbid if Android can do this well on its own
// Load in the full bitmap
Resources resources = getResources();
Bitmap theSky = BitmapFactory.decodeResource(resources, resId);
// Get display info
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// "Support" for Nougat's multi-window mode
int multiWindowOffsetWidth = 0;
int multiWindowOffsetHeight = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
// Compensate for the status bar's height when we are on the top side of the window.
// Shouldn't this be a part of the display metrics report? ¯\_(ツ)_/¯
try {
multiWindowOffsetHeight = resources.getDimensionPixelSize(resources.getIdentifier("status_bar_height", "dimen", "android"));
} catch (Resources.NotFoundException ex) {
// Assume a status bar size of 25dp
multiWindowOffsetHeight = (int) Math.ceil(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, metrics));
}
// We need to do some weird compensation for multi-window mode. I honestly don't
// understand it and I'm sufficiently tired. Without these a few pixels on the
// bottom and right edges will be left unfilled.
multiWindowOffsetHeight += 5;
multiWindowOffsetWidth = 5;
}
// Magic number to get some important image elements onscreen (205 for 01d, 250 for 01n)
float magic = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 180, metrics);
// Create empty bitmap the size of the screen
// Add in the multi-window compensation if any
Bitmap scaledSky = Bitmap.createBitmap(metrics.widthPixels + multiWindowOffsetWidth, metrics.heightPixels + multiWindowOffsetHeight, Bitmap.Config.ARGB_8888);
// Use a canvas to draw on the bitmap
Canvas canvas = new Canvas(scaledSky);
// Find the scale necessary to stretch across the X axis
// Add in the magic number to compensate for the translation done in the matrix
float wScale = ((float) scaledSky.getWidth() + magic) / theSky.getWidth();
// Find the scale necessary to stretch across the Y axis
float hScale = (float) scaledSky.getHeight() / theSky.getHeight();
// Use the largest scale (smallest side) to ensure the canvas is completely filled
float bigScale = wScale > hScale ? wScale : hScale;
Matrix matrix = new Matrix();
// Uniform scaling with the magic translation
matrix.setScale(bigScale, bigScale, magic, 0);
canvas.setMatrix(matrix);
// Draw the bitmap
canvas.drawBitmap(theSky, 0, 0, new Paint());
theSky.recycle();
return scaledSky;
}
@Override
protected void onPostExecute(Bitmap scaledSky) {
Drawable current = suchBg.getDrawable();
if(current != null) {
if(current instanceof TransitionDrawable) {
current = ((TransitionDrawable) current).getDrawable(1);
}
Drawable[] drawables = new Drawable[] { current, new BitmapDrawable(getResources(), scaledSky)};
TransitionDrawable transition = new TransitionDrawable(drawables);
transition.setCrossFadeEnabled(true);
suchBg.setImageDrawable(transition);
transition.startTransition(getResources().getInteger(R.integer.anim_refresh_time) * 2);
} else {
suchBg.setImageDrawable(new BitmapDrawable(getResources(), scaledSky));
}
}
}
private final class GetWeather extends AsyncTask<Location, Void, Object[]> {
@Override
protected void onPreExecute() {
// Dismiss the snackbar and dialog, if any
if (rationaleDialog != null && rationaleDialog.isShowing()) {
rationaleDialog.dismiss();
}
if (locationSnackbar != null && locationSnackbar.isShown()) {
locationSnackbar.dismiss();
}
}
private final String TAG = GetWeather.class.getSimpleName();
@Override
protected Object[] doInBackground(Location... params) {
if(!Geocoder.isPresent() && forceLocation.isEmpty()) {
return new Object[] { new UnsupportedOperationException(getString(R.string.geocoder_error_code)) };
}
WeatherUtil.WeatherData data;
if(forceLocation.isEmpty()) {
if(params[0] != null) {
data = Cache.getWeatherData(MainActivity.this, params[0].getLatitude(), params[0].getLongitude());
} else {
return new Object[] { new RuntimeException(getString(R.string.error_ensure_location_settings)) };
}
} else {
data = Cache.getWeatherData(MainActivity.this, forceLocation);
}
if(data == null || data.source != weatherSource) {
WeatherUtil.WeatherResult result;
if(forceLocation.isEmpty()) {
result = WeatherUtil.getWeather(params[0].getLatitude(), params[0].getLongitude(), weatherSource);
} else {
result = WeatherUtil.getWeather(forceLocation, weatherSource);
}
if(result.error != WeatherUtil.WeatherResult.ERROR_NONE) {
return new Object[] { result };
}
data = result.data;
Cache.putWeatherData(MainActivity.this, data);
}
Log.d(TAG, data.toString());
Address addr = null;
if(forceLocation.isEmpty()) {
addr = getAddress(data.latitude, data.longitude);
}
// Start the first half of the animation update still in the background thread
if (currentlyAnim) {
return new Object[] { null };
}
currentlyAnim = true;
if (forceLocation.isEmpty() && addr != null) {
String locality = addr.getLocality();
currentLocation = locality == null ? data.place : locality;
} else {
currentLocation = data.place;
}
String[] tempAdjs = getResources().getStringArray(WeatherDoge.getTempAdjectives((int)data.temperature));
String[] bgAdjs = getResources().getStringArray(WeatherDoge.getBgAdjectives(data.image));
weatherAdjectives = ArrayUtils.addAll(tempAdjs, bgAdjs);
if (data.link != null) {
Uri link = Uri.parse(data.link);
if ("http".equals(link.getScheme()) || "https".equals(link.getScheme())) {
currentLink = link;
}
}
return new Object[] { data, addr };
}
private Address getAddress(double latitude, double longitude) {
Geocoder geocoder = new Geocoder(MainActivity.this);
try {
List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);
if(addresses == null || addresses.size() == 0) {
return null;
}
return addresses.get(0);
} catch (Exception ex) {
if(ex.getMessage() != null && ex.getMessage().equalsIgnoreCase("Service not Available")) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if(errorDialog != null && errorDialog.isShowing()) {
return;
}
AlertDialog.Builder adb = new AlertDialog.Builder(new ContextThemeWrapper(MainActivity.this, R.style.AppTheme_Options));
adb.setTitle(R.string.geocoder_error_title).setMessage(R.string.geocoder_error_msg);
adb.setNeutralButton(R.string.wow, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
// Prevent crash if MainActivity is finishing while attempting to display a new dialog
if(!isFinishing()) {
errorDialog = adb.show();
}
}
});
}
Log.wtf(TAG, ex);
return null;
}
}
@Override
protected void onPostExecute(Object[] result) {
if (result[0] instanceof Throwable) {
Log.wtf(TAG, (Throwable) result[0]);
Toast.makeText(MainActivity.this, ((Throwable)result[0]).getMessage(), Toast.LENGTH_LONG).show();
return;
}
else if (result[0] instanceof WeatherUtil.WeatherResult) {
WeatherUtil.WeatherResult wResult = (WeatherUtil.WeatherResult)result[0];
switch (wResult.error) {
case WeatherUtil.WeatherResult.ERROR_API:
Log.e(TAG, wResult.msg);
Toast.makeText(MainActivity.this, wResult.msg, Toast.LENGTH_LONG).show();
break;
case WeatherUtil.WeatherResult.ERROR_THROWABLE:
String errorMsg = wResult.msg != null ? wResult.msg : wResult.throwable.getMessage();
Log.e(TAG, errorMsg, wResult.throwable);
Toast.makeText(MainActivity.this, errorMsg, Toast.LENGTH_LONG).show();
break;
}
return;
}
else if (!(result[0] instanceof WeatherUtil.WeatherData)) {
return;
}
// This is a continuation of the animation work started in the background thread
// The stuff here must be done on the current UI thread
WeatherUtil.WeatherData data = (WeatherUtil.WeatherData)result[0];
// Do the doge and background animations
new SetDogeTask(WeatherDoge.dogeSelect(data.image)).execute();
new SetBackgroundTask(WeatherDoge.skySelect(data.image)).execute();
// Do we need to animate?
String description = getString(R.string.wow) + " " + data.condition.trim().toLowerCase();
if (suchStatus.getText().equals(description) && (currentTemp == data.temperature) &&
(currentlyMetric != (UnitLocale.getDefault() == UnitLocale.IMPERIAL && !forceMetric)) &&
suchLocation.getText().equals(currentLocation)) {
currentlyAnim = false;
return;
}
// Do the second half of the animation work on a background thread
new WeatherDataTask().execute(data, description);
}
}
private final class WeatherDataTask extends AsyncTask<Object, Void, Object[]> {
@Override
protected Object[] doInBackground(Object... params) {
final WeatherUtil.WeatherData data = (WeatherUtil.WeatherData)params[0];
final String description = (String)params[1];
final FormattedTemp formattedTemp = new FormattedTemp(data.temperature);
final int animTime = (int)(getResources().getInteger(R.integer.anim_refresh_time) / 2.5);
final Animation[] fadeOuts = { AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_out),
AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_out),
AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_out) };
final Animation[] fadeIns = { AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_in),
AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_in),
AnimationUtils.loadAnimation(MainActivity.this, R.anim.textfade_in) };
final Handler uiHandler = new Handler(Looper.getMainLooper());
final Runnable tempGroupRunnable = new Runnable() {
@Override
public void run() {
suchTempGroup.startAnimation(fadeOuts[1]);
}
};
final Runnable locationRunnable = new Runnable() {
@Override
public void run() {
suchLocation.startAnimation(fadeOuts[2]);
}
};
fadeOuts[0].setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
uiHandler.postDelayed(tempGroupRunnable, animTime);
}
@Override
public void onAnimationEnd(Animation animation) {
suchStatus.setText(description);
suchStatus.startAnimation(fadeIns[0]);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
fadeOuts[1].setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
uiHandler.postDelayed(locationRunnable, animTime);
}
@Override
public void onAnimationEnd(Animation animation) {
currentTemp = data.temperature;
currentlyMetric = UnitLocale.getDefault() != UnitLocale.IMPERIAL || forceMetric;
suchTemp.setText(formattedTemp.str);
suchNegative.setVisibility(formattedTemp.temp < 0d ? View.VISIBLE : View.GONE);
suchDegree.setVisibility(View.VISIBLE);
suchTempGroup.startAnimation(fadeIns[1]);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
fadeOuts[2].setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
suchLocation.setText(currentLocation);
suchLocation.startAnimation(fadeIns[2]);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
fadeIns[2].setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
currentlyAnim = false;
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ParticleSystem newPartSys = null;
if (WeatherDoge.isSnowing(currentBackgroundId) && enableParticles
&& (particleSystem == null || !isEmitting)) {
newPartSys = newParticleSystem();
}
return new Object[] { fadeOuts[0], newPartSys };
}
private final class FormattedTemp {
private final double temp;
private final String str;
FormattedTemp(double inTemp) {
if (UnitLocale.getDefault() == UnitLocale.IMPERIAL && !forceMetric) {
inTemp = inTemp * 1.8d + 32d; // F
}
inTemp = Math.round(inTemp);
DecimalFormat df = new DecimalFormat();
df.setNegativePrefix("");
df.setNegativeSuffix("");
df.setMaximumFractionDigits(0);
df.setDecimalSeparatorAlwaysShown(false);
df.setGroupingUsed(false);
temp = inTemp;
str = df.format(inTemp);
}
}
@Override
protected void onPostExecute(Object[] objects) {
if (WeatherDoge.isSnowing(currentBackgroundId) && enableParticles) {
if (!isEmitting) {
if (particleSystem != null) {
particleSystem.cancel();
}
particleSystem = (ParticleSystem)objects[1];
startParticleSystem();
}
} else if (particleSystem != null && isEmitting) {
particleSystem.stopEmitting();
isEmitting = false;
}
suchStatus.startAnimation((Animation)objects[0]);
}
}
}