From 8a208283ee472ab22f90ae848caf99876bd47097 Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Wed, 22 Jan 2025 14:05:38 +0100 Subject: [PATCH] Better data display, tabs for each day, preferences --- app/src/main/AndroidManifest.xml | 16 +- .../one00kb/minipogodynka/MainActivity.java | 147 ++++++++++++------ .../minipogodynka/PreferenceHolder.java | 54 +++++++ .../minipogodynka/SettingsActivity.java | 15 ++ .../eur/one00kb/minipogodynka/Utilities.java | 14 ++ .../minipogodynka/WeatherDataAdapter.java | 36 +++++ .../weather/WeatherAPIFetcher.java | 38 +++++ .../minipogodynka/weather/WeatherData.java | 44 ++---- .../weather/WeatherJSONParser.java | 63 ++++++++ app/src/main/res/layout/activity_main.xml | 27 ---- app/src/main/res/layout/item_weather.xml | 35 +++++ app/src/main/res/layout/tab_weather.xml | 7 + app/src/main/res/menu/menu_main.xml | 20 +++ app/src/main/res/values/arrays.xml | 34 ++++ app/src/main/res/values/strings.xml | 24 +++ app/src/main/res/xml/preferences.xml | 26 ++++ gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 18 files changed, 495 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/PreferenceHolder.java create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/SettingsActivity.java create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/Utilities.java create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/WeatherDataAdapter.java create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherAPIFetcher.java create mode 100644 app/src/main/java/eur/one00kb/minipogodynka/weather/WeatherJSONParser.java delete mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/item_weather.xml create mode 100644 app/src/main/res/layout/tab_weather.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/preferences.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e9fc751..dbb0b6c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,22 +2,30 @@ - - + - + + + + + + \ No newline at end of file diff --git a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java index 861b45b..c9ef488 100644 --- a/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java +++ b/app/src/main/java/eur/one00kb/minipogodynka/MainActivity.java @@ -3,54 +3,91 @@ package eur.one00kb.minipogodynka; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.app.Activity; +import android.app.TabActivity; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.location.Address; +import android.location.Geocoder; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; -import android.widget.Button; -import android.widget.TextView; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.GridView; +import android.widget.TabHost; -import org.xml.sax.InputSource; - -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.net.MalformedURLException; -import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import java.util.Locale; +import java.util.Map; +import eur.one00kb.minipogodynka.weather.WeatherAPIFetcher; import eur.one00kb.minipogodynka.weather.WeatherData; +import eur.one00kb.minipogodynka.weather.WeatherJSONParser; -public class MainActivity extends Activity implements LocationListener { - private static final String API_FORMAT = "https://api.open-meteo" + - ".com/v1/forecast?latitude=%f&longitude=%f¤t=temperature_2m," + - "relative_humidity_2m,rain,surface_pressure,wind_speed_10m"; - +/** + * @noinspection deprecation + */ +public class MainActivity extends TabActivity implements LocationListener, TabHost.TabContentFactory { + private PreferenceHolder mPreferenceHolder; private LocationManager mLocManager; - private Button mUpdateButton; + private Geocoder mGeocoder; - private double mLat, mLong; - private WeatherData mData; + private Location mCurrentLocation; + private Address mCurrentAddress; + private Map mWeatherData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mData = new WeatherData(); + mPreferenceHolder = new PreferenceHolder(this); mLocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + mGeocoder = new Geocoder(this, Locale.getDefault()); - setContentView(R.layout.activity_main); - mUpdateButton = findViewById(R.id.btn_update); - mUpdateButton.setOnClickListener((button) -> updateWeather()); + setDefaultTab(0); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_locate) { + updateWeather(); + } else if (id == R.id.action_settings) { + Intent itn = new Intent(this, SettingsActivity.class); + startActivity(itn); + } + + return super.onOptionsItemSelected(item); + } + + private void createTabsForDays(Date date) { + TabHost host = getTabHost(); + host.clearAllTabs(); + + for (int i = 0; i < mPreferenceHolder.getForecastDays(); ++i) { + host.addTab(host.newTabSpec(Integer.toString(i)) + .setIndicator(String.format("%ta", date)).setContent(this)); + date.setDate(date.getDate() + 1); + } } @TargetApi(23) private void checkAndRequestPerms() { if (isLocationAllowed()) { - requestPermissions(new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION }, 0); + requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0); } } @@ -59,6 +96,7 @@ public class MainActivity extends Activity implements LocationListener { checkAndRequestPerms(); // First, we try to get the location of the user. if (isLocationAllowed()) { + setTitle("Locating…"); mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); } } @@ -70,39 +108,58 @@ public class MainActivity extends Activity implements LocationListener { } private void updateInfo() { - TextView test1 = findViewById(R.id.temp_day); - TextView test2 = findViewById(R.id.temp_night); - test1.setText(mData.getTemperature()); - test2.setText(mData.getTemperature()); + if (mCurrentAddress != null) { + setTitle(mCurrentAddress.getLocality()); + } else { + setTitle("Unknown location"); + } + + createTabsForDays(Utilities.getDateOnly()); } private void getJson() { new Thread(() -> { - try { - URL url = new URL(String.format(Locale.ROOT, API_FORMAT, mLat, mLong)); - BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); - StringBuilder builder = new StringBuilder(); - - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); - } - reader.close(); - mData.fromJson(builder.toString()); - runOnUiThread(this::updateInfo); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } + String apiData = WeatherAPIFetcher.fetchOpenMeteo(mCurrentLocation.getLatitude(), + mCurrentLocation.getLongitude()); + mWeatherData = WeatherJSONParser.parseOpenMeteo(apiData); + runOnUiThread(this::updateInfo); }).start(); } @Override public void onLocationChanged(Location location) { - mLat = location.getLatitude(); - mLong = location.getLongitude(); + mCurrentLocation = location; mLocManager.removeUpdates(this); + + try { + List
addresses = mGeocoder.getFromLocation(location.getLatitude(), + location.getLongitude(), 1); + if (addresses != null && addresses.size() > 0) { + mCurrentAddress = addresses.get(0); + } + } catch (IOException e) { + e.printStackTrace(); + } + getJson(); } + + @Override + public View createTabContent(String tag) { + GridView view = (GridView) getLayoutInflater().inflate(R.layout.tab_weather, null); + + Date forApiDate = Utilities.getDateOnly(); + forApiDate.setDate(forApiDate.getDate() + Integer.parseInt(tag)); + ArrayList forAdapter = new ArrayList<>(); + + for (int i = forApiDate.getHours(); i < 24; i += mPreferenceHolder.getForecastType()) { + forApiDate.setHours(i); + forAdapter.add(mWeatherData.get(forApiDate)); + } + + WeatherDataAdapter adapter = new WeatherDataAdapter(this, forAdapter); + view.setAdapter(adapter); + + return view; + } } diff --git a/app/src/main/java/eur/one00kb/minipogodynka/PreferenceHolder.java b/app/src/main/java/eur/one00kb/minipogodynka/PreferenceHolder.java new file mode 100644 index 0000000..2beba1f --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/PreferenceHolder.java @@ -0,0 +1,54 @@ +package eur.one00kb.minipogodynka; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import java.util.Map; + +/** + * @noinspection deprecation + */ +public class PreferenceHolder implements SharedPreferences.OnSharedPreferenceChangeListener { + private final Map mPreferenceValues; + private final Context mContext; + + @SuppressWarnings("unchecked") //NON-NLS + public PreferenceHolder(Context context) { + mContext = context; + SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + PreferenceManager.setDefaultValues(mContext, R.xml.preferences, false); + mPreferences.registerOnSharedPreferenceChangeListener(this); + mPreferenceValues = (Map) mPreferences.getAll(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + Object value = sharedPreferences.getAll().get(key); + if (value != null) { + mPreferenceValues.put(key, value); + } + } + + public Object getOrDefault(String key, Object defaultValue) { + Object value = mPreferenceValues.get(key); + return value == null ? defaultValue : value; + } + + public String getProvider() { + return (String) getOrDefault(mContext.getString(R.string.preferences_key_provider), + mContext.getString(R.string.preferences_default_provider)); + } + + public int getForecastDays() { + return Integer.parseInt((String) + getOrDefault(mContext.getString(R.string.preferences_key_forecast_days), + mContext.getString(R.string.preferences_default_forecast_days))); + } + + public int getForecastType() { + return Integer.parseInt((String) + getOrDefault(mContext.getString(R.string.preferences_key_forecast_type), + mContext.getString(R.string.preferences_default_forecast_type))); + } +} \ No newline at end of file diff --git a/app/src/main/java/eur/one00kb/minipogodynka/SettingsActivity.java b/app/src/main/java/eur/one00kb/minipogodynka/SettingsActivity.java new file mode 100644 index 0000000..1d9d269 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/SettingsActivity.java @@ -0,0 +1,15 @@ +package eur.one00kb.minipogodynka; + +import android.os.Bundle; +import android.preference.PreferenceActivity; + +/** + * @noinspection deprecation + */ +public class SettingsActivity extends PreferenceActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java b/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java new file mode 100644 index 0000000..1169693 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/Utilities.java @@ -0,0 +1,14 @@ +package eur.one00kb.minipogodynka; + +import java.util.Date; + +public class Utilities { + public static Date getDateOnly() { + Date date = new Date(); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setTime(date.getTime() - (date.getTime() % 1000)); + return date; + } +} diff --git a/app/src/main/java/eur/one00kb/minipogodynka/WeatherDataAdapter.java b/app/src/main/java/eur/one00kb/minipogodynka/WeatherDataAdapter.java new file mode 100644 index 0000000..f7b2387 --- /dev/null +++ b/app/src/main/java/eur/one00kb/minipogodynka/WeatherDataAdapter.java @@ -0,0 +1,36 @@ +package eur.one00kb.minipogodynka; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.ArrayList; + +import eur.one00kb.minipogodynka.weather.WeatherData; + +public class WeatherDataAdapter extends ArrayAdapter { + public WeatherDataAdapter(Context context, ArrayList wdList) { + super(context, 0, wdList); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_weather, parent, false); + } + + WeatherData data = getItem(position); + TextView time = convertView.findViewById(R.id.time); + TextView temperature = convertView.findViewById(R.id.temperature); + TextView precipitation = convertView.findViewById(R.id.precipitation); + + time.setText(String.format("%tH:% parseOpenMeteo(String json) { + HashMap hours = new HashMap<>(); + + try { + JSONObject root = new JSONObject(json); + JSONObject hourly = root.getJSONObject("hourly"); + + JSONArray times = hourly.getJSONArray("time"); + JSONArray temperatures = hourly.getJSONArray("temperature_2m"); + JSONArray humidities = hourly.getJSONArray("relative_humidity_2m"); + JSONArray precipitations = hourly.getJSONArray("precipitation"); + JSONArray pressures = hourly.getJSONArray("pressure_msl"); + JSONArray cloudCovers = hourly.getJSONArray("cloud_cover"); + JSONArray windSpeeds = hourly.getJSONArray("wind_speed_10m"); + JSONArray windDirections = hourly.getJSONArray("wind_direction_10m"); + JSONArray uvIndexes = hourly.getJSONArray("uv_index"); + + for (int i = 0; i < times.length(); ++i) { + WeatherData data = new WeatherData(); + data.date = OM_DATEFORMAT.parse(times.getString(i)); + data.temperature = temperatures.getDouble(i); + data.humidity = humidities.getDouble(i); + data.precipitation = precipitations.getDouble(i); + data.pressure = pressures.getDouble(i); + data.cloudCover = cloudCovers.getDouble(i); + data.windSpeed = windSpeeds.getDouble(i); + data.windDirection = windDirections.getDouble(i); + data.uvIndex = uvIndexes.getDouble(i); + + hours.put(OM_DATEFORMAT.parse(times.getString(i)), data); + } + } catch (JSONException | ParseException e) { + e.printStackTrace(); + return null; + } + + return hours; + } + + public static void parseMetNo() { + + } + + public static void parseMeteoFrance() { + + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index ccf01aa..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - -