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 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_weather.xml b/app/src/main/res/layout/item_weather.xml
new file mode 100644
index 0000000..eeeaa69
--- /dev/null
+++ b/app/src/main/res/layout/item_weather.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/tab_weather.xml b/app/src/main/res/layout/tab_weather.xml
new file mode 100644
index 0000000..7e1a061
--- /dev/null
+++ b/app/src/main/res/layout/tab_weather.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..b9324fc
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..4aabbe7
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,34 @@
+
+
+
+ - Open-Meteo
+ - Meteorologisk institutt (MET)
+ - Météo-France
+
+
+
+ - om
+ - met
+ - mf
+
+
+
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+
+
+
+ - Sparse
+ - Regular
+ - Detailed
+
+
+
+ - 6
+ - 3
+ - 1
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f95482d..0e5646e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,28 @@
MiniPogodynka
+ Settings
+
+ Placeholder
+
+ Update
+ Search
+ Locate
+ Settings
+
+ weather_provider
+ Weather provider
+ om
+
+ forecast_days
+ Forecast days
+ 3
+
+ forecast_type
+ Forecast type
+ 6
+
+
+ Copyleft 🄯 Emil Kosz, 2025. License: GPLv3
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..8dc0b61
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5d03cf1..73f832c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.4.1"
+agp = "8.7.3"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 802860e..b52fc0f 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed Jan 15 14:30:26 GMT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists