Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue-639: Current day warnings widget #737

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2450e6d
issue-639: Warning icons added.
stembitf Dec 10, 2024
92dd088
issue-639: First draft of small warnings widget layout.
stembitf Dec 10, 2024
99700ab
issue-639: First draft of all needed files.
stembitf Dec 10, 2024
fd02ebd
issue-639: First draft of all needed files.
stembitf Dec 10, 2024
38eadc0
issue-639: Some renaming done for clarification.
stembitf Dec 10, 2024
b242dd9
issue-639: Separate base class for warnings widgets.
stembitf Dec 10, 2024
c28295c
Merge branch 'android-main' into feature/issue-639-current-day-warnin…
stembitf Dec 11, 2024
07e3bd8
issue-639: Code cleanup.
stembitf Dec 11, 2024
7ecb3aa
issue-639: First draft of methods in BaseWarningsWidgetProvider. Some…
stembitf Dec 12, 2024
3016e09
issue-639: Warning widget working to some extent.
stembitf Dec 13, 2024
1e9da51
issue-639: more weather icon mapping
stembitf Dec 13, 2024
3542c35
issue-639: Warning widget working basically.
stembitf Dec 16, 2024
115c2ad
issue-639: Favorite locations taken into use for warning widget.
stembitf Dec 16, 2024
f65d4e4
issue-639: Separate update scheduling for warning widgets.
stembitf Dec 17, 2024
ad56c8c
issue-639: Attempt of new small widget layout, which fills cells.
stembitf Dec 17, 2024
0b6081c
Revert "issue-639: Attempt of new small widget layout, which fills ce…
stembitf Dec 17, 2024
b1014d9
issue-639: Warning titles
stembitf Dec 17, 2024
9df97eb
issue-639: Text color handling for different themes
stembitf Dec 17, 2024
fb63cff
issue-639: UI changes and widget reset.
stembitf Dec 17, 2024
6ead16b
issue-639: start date added when needed for single warning.
stembitf Dec 18, 2024
ddf1205
issue-639: Fix for scheduled widget updates.
stembitf Dec 18, 2024
b82f996
issue-639: Clearing widget updates changed.
stembitf Dec 18, 2024
ece0ee7
issue-639: Widget setup and widget disable changes.
stembitf Dec 18, 2024
d0e4ca7
issue-639: Show only warnings which are valid now.
stembitf Dec 19, 2024
e5524a2
issue-639: Filtering code extracted to methods
stembitf Dec 19, 2024
2bd2b8c
issue-639: New icon for sea wind with direction and intensity.
stembitf Dec 19, 2024
4a2fc6c
issue-639: Merged main, fixed conflicts
geosaaga Dec 20, 2024
3de1c88
issue-639: Renamed xml-v31 files according to new naming
geosaaga Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/small_widget_provider_info" />
<meta-data android:name="android.appwidget.provider" android:resource="@xml/small_forecast_widget_provider_info" />
</receiver>
<activity android:name=".SmallForecastWidgetConfigurationActivity" android:exported="false">
<intent-filter>
Expand All @@ -74,7 +74,7 @@
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/large_widget_provider_info" />
<meta-data android:name="android.appwidget.provider" android:resource="@xml/large_forecast_widget_provider_info" />
</receiver>
<activity android:name=".LargeForecastWidgetConfigurationActivity" android:exported="false">
<intent-filter>
Expand All @@ -89,14 +89,29 @@
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/max_widget_provider_info" />
<meta-data android:name="android.appwidget.provider" android:resource="@xml/max_forecast_widget_provider_info" />
</receiver>
<activity android:name=".MaxForecastWidgetConfigurationActivity" android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<receiver android:name=".SmallWarningsWidgetProvider" android:label="@string/small_warnings_widget" android:exported="false">
<intent-filter>
<action android:name="fi.fmi.mobileweather.AUTO_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/small_warnings_widget_provider_info" />
</receiver>
<activity android:name=".SmallWarningsWidgetConfigurationActivity" android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
package fi.fmi.mobileweather;

import static android.text.format.DateUtils.isToday;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;

import static fi.fmi.mobileweather.ColorUtils.getPrimaryBlue;
import static fi.fmi.mobileweather.PrefKey.FAVORITE_LATLON;
import static fi.fmi.mobileweather.Theme.LIGHT;

import android.graphics.Color;
import android.util.Log;
import android.widget.RemoteViews;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public abstract class BaseWarningsWidgetProvider extends BaseWidgetProvider {

@Override
protected void executeDataFetchingWithSelectedLocation(int geoId, SharedPreferencesHelper pref, int widgetId) {
String latlon = pref.getString(FAVORITE_LATLON, null);
executeDataFetchingWithLatLon(latlon, pref, widgetId);
}

@Override
protected void executeDataFetchingWithLatLon(String latlon, SharedPreferencesHelper pref, int widgetId) {

// if we have no location, do not update the widget
if (latlon == null || latlon.isEmpty()) {
Log.d("Warnings Widget Update", "No location data available, widget not updated");
return;
}

ExecutorService executorService = Executors.newFixedThreadPool(3);

// Get language string
String language = getLanguageString();

// temporary for testing.
// String announceUrl = "https://en-beta.ilmatieteenlaitos.fi/api/general/mobileannouncements";
String announceUrl = announcementsUrl;

// get warnings data bases on latlon
Future<JSONObject> future1 = executorService.submit(() -> fetchMainData(latlon, language));
// get announcements
Future<JSONArray> future2 = executorService.submit(() -> fetchJsonArray(announceUrl));
// get location name and region
Future<String> future3 = executorService.submit(() -> fetchLocationData(latlon));

executorService.submit(() -> {
try {
JSONObject result1 = future1.get();
JSONArray result2 = future2.get();
String result3 = future3.get();
onDataFetchingPostExecute(result1, result2, result3, null, pref, widgetId);
} catch (Exception e) {
Log.e("Warnings Widget Update", "Exception: " + e.getMessage());
// NOTE: let's not show error view here, because connection problems with server
// seem to be quite frequent and we don't want to show error view every time
}
});
}

private String fetchLocationData(String latlon) {
// example: ?param=geoid,name,region,latitude,longitude,region,country,iso2,localtz&latlon=62.5,26.2&format=json
String url = weatherUrl
+ "?param=geoid,name,region,latitude,longitude,region,country,iso2,localtz&latlon="
+ latlon
+ "&format=json";
try {
return fetchJsonString(url);
} catch (Exception e) {
Log.e("Warnings Widget Update", "fetchLocationData exception: " + e.getMessage());
return null;
}
}

protected JSONObject fetchMainData(String latlon, String language) {

String url;
if (latlon != null && !latlon.isEmpty()) {
url = warningsUrl + "?latlon=" +
latlon +
"&country=" +
language +
"&who=mobileweather-widget-android";
} else {
return null;
}

try {
String jsonString = fetchJsonString(url);
return new JSONObject(jsonString);
} catch (JSONException e) {
Log.e("Warnings Widget Update", "In base warnings fetchMainData exception: " + e.getMessage());
return null;
}
}

@Override
protected void setWidgetUi(JSONArray announcementsJson, SharedPreferencesHelper pref, WidgetInitResult widgetInitResult, int widgetId, String locationJson) {

Log.d("Warnings Widget Update", "setWidgetUi called");

RemoteViews widgetRemoteViews = widgetInitResult.widgetRemoteViews();
JSONObject warningsJsonObj = widgetInitResult.mainJson();
String theme = widgetInitResult.theme();

if (warningsJsonObj == null) {
return;
}

try {

Gson gson = new Gson();

Type locationListType = new TypeToken<List<LocationRecord>>() {}.getType();
List<LocationRecord> locations = gson.fromJson(locationJson, locationListType);
LocationRecord location = locations.get(0);

WarningsRecordRoot warningsRecordRoot = gson.fromJson(warningsJsonObj.toString(), WarningsRecordRoot.class);

Log.d("Warnings Widget Update", "WarningsJson: " + warningsJsonObj);
Log.d("Warnings Widget Update", "WarningsRecordRoot: " + warningsRecordRoot);


// filter out the warnings by language
filterByLanguage(warningsRecordRoot);

// filter the warnings that only the ones remain if now is between the start and end date
// (perhaps npt this: or if the warning starts today later)
filterByValidity(warningsRecordRoot);

// reset the warning icon layouts to GONE first
resetWidgetUi(widgetRemoteViews);

// show a maximum of 3 warnings
int amountOfWarnings = warningsRecordRoot.data().warnings().size();
Log.d("Warnings Widget Update", "Amount of warnings: " + amountOfWarnings);
int amountOfWarningsToShow = Math.min(amountOfWarnings, 3);
Log.d("Warnings Widget Update", "Amount of warnings to show: " + amountOfWarningsToShow);
for (int i = 0; i < amountOfWarningsToShow; i++) {
Warning warning = warningsRecordRoot.data().warnings().get(i);
String type = warning.type();
String severity = warning.severity();
String startTime = warning.duration().startTime();
String endTime = warning.duration().endTime();

// *** set the warning data to the widget

// Set the location name and region
widgetRemoteViews.setTextViewText(R.id.locationNameTextView, location.name());
widgetRemoteViews.setTextViewText(R.id.locationRegionTextView, location.region());

// make the warning icon layout visible
int warningIconLayoutId = context.getResources().getIdentifier("warningIconLayout" + i, "id", context.getPackageName());
widgetRemoteViews.setViewVisibility(warningIconLayoutId, VISIBLE);

// set the background circles
int circleBackgroundId = context.getResources().getIdentifier("warningIconBackgroundImageView" + i, "id", context.getPackageName());
Log.d("Warnings Widget Update", "circleBackgroundId: " + circleBackgroundId);
int circleBackgroundResourceId = WarningsIconMapper.getCircleBackgroundResourceId(severity);
Log.d("Warnings Widget Update", "CircleBackgroundResourceId: " + circleBackgroundResourceId);
if (circleBackgroundResourceId != 0) {
widgetRemoteViews.setInt(circleBackgroundId, "setBackgroundResource", circleBackgroundResourceId);
}

// Set the icons in the layout
int warningIconImageViewId = context.getResources().getIdentifier("warningIconImageView" + i, "id", context.getPackageName());
Log.d("Warnings Widget Update", "warningIconImageViewId: " + warningIconImageViewId);
// set the warning icon based on the warning type
if (type.equals("seaWind")) {
int windIntensity = warning.physical().windIntensity();
int windDirection = warning.physical().windDirection();

widgetRemoteViews.setImageViewResource(warningIconImageViewId, R.drawable.sea_wind);
// rotate the sea wind image view based on the wind direction number
widgetRemoteViews.setFloat(warningIconImageViewId, "setRotation", windDirection);
// add the wind intensity text in front of the image view
int windIntensityTextViewId = context.getResources().getIdentifier("windIntensityTextView" + i, "id", context.getPackageName());
widgetRemoteViews.setViewVisibility(windIntensityTextViewId, VISIBLE);
widgetRemoteViews.setTextViewText(windIntensityTextViewId, Integer.toString(windIntensity));
} else {
int iconResourceId = WarningsIconMapper.getIconResourceId(type);
Log.d("Warnings Widget Update", "IconResourceId: " + iconResourceId);
if (iconResourceId != 0) {
widgetRemoteViews.setImageViewResource(warningIconImageViewId, iconResourceId);
}
}

// set warning text and time frame colors based on theme
int textColor = theme.equals(LIGHT) ? getPrimaryBlue(context) : Color.WHITE;
widgetRemoteViews.setInt(R.id.warningTextView, "setTextColor", textColor);
widgetRemoteViews.setInt(R.id.warningTimeFrameTextView, "setTextColor", textColor);

// if there is only one warning, set the warning 'title' to the first warning
if (amountOfWarningsToShow == 1) {
String warningTitle = context.getString(WarningsTextMapper.getStringResourceId(type));
widgetRemoteViews.setTextViewText(R.id.warningTextView, warningTitle);
widgetRemoteViews.setTextViewText(R.id.warningTimeFrameTextView, getFormattedWarningTimeFrame(startTime, endTime));
// Show also the time fame
widgetRemoteViews.setViewVisibility(R.id.warningTimeFrameTextView, VISIBLE);
widgetRemoteViews.setTextViewText(R.id.warningTimeFrameTextView, getFormattedWarningTimeFrame(startTime, endTime));
}
}

// if there is more than one warning, set the warning text to "Warnings (amount)" and do show time frame
if (amountOfWarningsToShow > 1) {
String warningsText = context.getResources().getString(R.string.warnings) + " (" + amountOfWarnings + ")";
widgetRemoteViews.setTextViewText(R.id.warningTextView, warningsText);
}

// if there are no warnings, show "No warnings"
if (amountOfWarningsToShow == 0) {
String warningsText = context.getResources().getString(R.string.no_warnings);
widgetRemoteViews.setTextViewText(R.id.warningTextView, warningsText);
}

// Crisis view
showCrisisViewIfNeeded(announcementsJson, widgetRemoteViews, pref, true);

// Update time TODO: should be hidden for release
widgetRemoteViews.setTextViewText(R.id.updateTimeTextView, DateFormat.getTimeInstance().format(new Date()));

appWidgetManager.updateAppWidget(widgetId, widgetRemoteViews);

} catch (Exception e) {
Log.e("Warnings Widget Update", "In base warnings setWidgetUi exception: " + e.getMessage());
showErrorView(
context,
pref,
context.getResources().getString(R.string.update_failed),
context.getResources().getString(R.string.check_internet_connection),
widgetId
);
}
}

private void filterByValidity(WarningsRecordRoot warningsRecordRoot) throws ParseException {
Iterator<Warning> iterator2 = warningsRecordRoot.data().warnings().iterator();
while (iterator2.hasNext()) {
Warning warning = iterator2.next();
String startTime = warning.duration().startTime();
String endTime = warning.duration().endTime();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
Date startDate = formatter.parse(startTime);
Date endDate = formatter.parse(endTime);
if (!isNowValid(startDate, endDate) /*&& !startsTodayLater(startDate)*/) {
iterator2.remove();
}
}
}

private void filterByLanguage(WarningsRecordRoot warningsRecordRoot) {
Iterator<Warning> iterator = warningsRecordRoot.data().warnings().iterator();
while (iterator.hasNext()) {
Warning warning = iterator.next();
if (!warning.language().equals(getLanguageString())) {
iterator.remove();
}
}
}

private boolean startsTodayLater(Date startDate) {
// check if the start time is today and in the future
return isToday(startDate.getTime()) && startDate.after(new Date());
}

private boolean isNowValid(Date startDate, Date endDate) {
Date now = new Date();
return now.after(startDate) && now.before(endDate);
}

private void resetWidgetUi(RemoteViews widgetRemoteViews) {
for (int i = 0; i < 3; i++) {
int warningIconLayoutId = context.getResources().getIdentifier("warningIconLayout" + i, "id", context.getPackageName());
widgetRemoteViews.setViewVisibility(warningIconLayoutId, GONE);
}
widgetRemoteViews.setViewVisibility(R.id.warningTimeFrameTextView, GONE);
}

protected String getFormattedWarningTimeFrame(String startTime, String endTime) throws ParseException {
// Define the input formatter
SimpleDateFormat inputFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
// Parse the time strings to Date
Date startDate = inputFormatter.parse(startTime);
Date endDate = inputFormatter.parse(endTime);

// Define the output formatter
SimpleDateFormat outputFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault());
SimpleDateFormat outputFormatterWithDate = new SimpleDateFormat("dd.MM. HH:mm", Locale.getDefault());

// if start date is not today, add the date to the output
if (startDate != null && !isToday(startDate.getTime())) {
return outputFormatterWithDate.format(startDate) + " - " + outputFormatter.format(endDate);
} else {
return outputFormatter.format(startDate) + " - " + outputFormatter.format(endDate);
}
}
}
Loading