commit df7c5e6e3fe99385f8093c8fd12fa13397745117 Author: Stepan Date: Mon Jun 23 23:56:50 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..32e05af --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Air Quality \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..97f0a8e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e0fd9f8 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.airquality" + compileSdk = 35 + + defaultConfig { + applicationId = "com.example.airquality" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + implementation("com.android.volley:volley:1.2.1") +} \ No newline at end of file diff --git a/app/moodle/AQActivity.java b/app/moodle/AQActivity.java new file mode 100644 index 0000000..e80b2e9 --- /dev/null +++ b/app/moodle/AQActivity.java @@ -0,0 +1,191 @@ +package com.example.airquality; + +import android.os.Bundle; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class AQActivity extends AppCompatActivity { + private static final Map cityCoordinates = new HashMap<>(); + static { + cityCoordinates.put("Subotica", new double[]{46.1, 19.6667}); + cityCoordinates.put("Novi Sad", new double[]{45.2517, 19.8369}); + cityCoordinates.put("Beograd", new double[]{44.804, 20.4651}); + cityCoordinates.put("San Francisko", new double[]{37.7749, -122.4194}); + cityCoordinates.put("Sidnej", new double[]{-33.8679, 151.2073}); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_aqactivity); + Toolbar toolbar = findViewById(R.id.toolbar); + + // Using toolbar as ActionBar + setSupportActionBar(toolbar); + + // Display application icon in the toolbar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowHomeEnabled(true); + getSupportActionBar().setLogo(R.drawable.weather_clear_symbolic); + getSupportActionBar().setDisplayUseLogoEnabled(true); + } + + loadAQ(); + } + @Override + protected void onResume() { + super.onResume(); + loadAQ(); + } + + public boolean onCreateOptionsMenu(android.view.Menu menu) { + MenuInflater inflater = getMenuInflater(); + + inflater.inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + + @Override + public boolean onOptionsItemSelected(android.view.MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_aq: + Intent intentAQ = new Intent(this, AQActivity.class); + startActivity(intentAQ); + return true; + case R.id.menu_settings: + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + private String getAQIDescription(int aqi) { + switch (aqi) { + case 1: return "Good"; + case 2: return "Fair"; + case 3: return "Moderate"; + case 4: return "Poor"; + case 5: return "Very Poor"; + default: return "Unknown"; + } + } + public void loadAQ(){ + String city = PreferencesManager.getCity(this); + double[] coords = cityCoordinates.get(city); + if (coords == null) { + Toast.makeText(this, "Unknown city: " + city, Toast.LENGTH_SHORT).show(); + return; + } + String apiKey = getString(R.string.weather_api_key); + String url = String.format( + "https://api.openweathermap.org/data/2.5/air_pollution?lat=%.6f&lon=%.6f&appid=%s", + coords[0], coords[1], apiKey + ); + + RequestQueue queue = Volley.newRequestQueue(this); + JsonObjectRequest request = new JsonObjectRequest( + Request.Method.GET, + url, + null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + // Get JSON fields + JSONObject listItem = response.getJSONArray("list").getJSONObject(0); + int aqi = listItem.getJSONObject("main").getInt("aqi"); + + View rootLayout = findViewById(R.id.main); + + int colorResId; + switch (aqi) { + case 1: + colorResId = R.color.aqi_good; + break; + case 2: + colorResId = R.color.aqi_fair; + break; + case 3: + colorResId = R.color.aqi_moderate; + break; + case 4: + colorResId = R.color.aqi_poor; + break; + case 5: + colorResId = R.color.aqi_very_poor; + break; + default: + colorResId = android.R.color.background_light; + } + + rootLayout.setBackgroundColor(getResources().getColor(colorResId)); + + JSONObject components = listItem.getJSONObject("components"); + double pm25 = components.getDouble("pm2_5"); + double pm10 = components.getDouble("pm10"); + double o3 = components.getDouble("o3"); + double co = components.getDouble("co"); + double no2 = components.getDouble("no2"); + + + TextView tvCity = findViewById(R.id.tvCity); + TextView tvAQ = findViewById(R.id.tvAQ); + TextView tvComponents = findViewById(R.id.tvComponents); + + + String city = PreferencesManager.getCity(AQActivity.this); + + + tvCity.setText("City: " + city); + tvAQ.setText("AQI Index: " + aqi + " (" + getAQIDescription(aqi) + ")"); + tvComponents.setText( + String.format("PM2.5: %.2f µg/m³\nPM10: %.2f µg/m³\nO₃: %.2f ppb\nCO: %.2f ppm\nNO₂: %.2f ppb", + pm25, pm10, o3, co, no2) + ); + + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(AQActivity.this, "Failed to parse air data", Toast.LENGTH_SHORT).show(); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Toast.makeText(AQActivity.this, "Failed to load air pollution data", Toast.LENGTH_SHORT).show(); + error.printStackTrace(); + } + } + ); + + queue.add(request); + + } +} \ No newline at end of file diff --git a/app/moodle/activity_aqactivity.xml b/app/moodle/activity_aqactivity.xml new file mode 100644 index 0000000..4f2bbb4 --- /dev/null +++ b/app/moodle/activity_aqactivity.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/moodle/air_quality_files.zip b/app/moodle/air_quality_files.zip new file mode 100644 index 0000000..534ec2f Binary files /dev/null and b/app/moodle/air_quality_files.zip differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/airquality/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/airquality/ExampleInstrumentedTest.java new file mode 100644 index 0000000..213f02f --- /dev/null +++ b/app/src/androidTest/java/com/example/airquality/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.airquality; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.airquality", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8d1082b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/forecast_sample_40.json b/app/src/main/assets/forecast_sample_40.json new file mode 100644 index 0000000..fd89808 --- /dev/null +++ b/app/src/main/assets/forecast_sample_40.json @@ -0,0 +1,159 @@ +{ + "cod": "200", + "message": 0, + "cnt": 8, + "list": [ + { + "dt": 1718325600, + "main": { + "temp": 298.15, + "pressure": 1013, + "humidity": 65 + }, + "weather": [ + { + "description": "clear sky", + "icon": "01d" + } + ], + "wind": { + "speed": 1.5, + "deg": 90 + }, + "dt_txt": "2025-06-14 00:00:00" + }, + { + "dt": 1718336400, + "main": { + "temp": 297.10, + "pressure": 1012, + "humidity": 68 + }, + "weather": [ + { + "description": "few clouds", + "icon": "02n" + } + ], + "wind": { + "speed": 1.8, + "deg": 100 + }, + "dt_txt": "2025-06-14 03:00:00" + }, + { + "dt": 1718347200, + "main": { + "temp": 296.00, + "pressure": 1011, + "humidity": 70 + }, + "weather": [ + { + "description": "scattered clouds", + "icon": "03n" + } + ], + "wind": { + "speed": 2.0, + "deg": 110 + }, + "dt_txt": "2025-06-14 06:00:00" + }, + { + "dt": 1718358000, + "main": { + "temp": 299.50, + "pressure": 1012, + "humidity": 60 + }, + "weather": [ + { + "description": "broken clouds", + "icon": "04d" + } + ], + "wind": { + "speed": 2.5, + "deg": 120 + }, + "dt_txt": "2025-06-14 09:00:00" + }, + { + "dt": 1718368800, + "main": { + "temp": 302.20, + "pressure": 1013, + "humidity": 50 + }, + "weather": [ + { + "description": "light rain", + "icon": "10d" + } + ], + "wind": { + "speed": 2.8, + "deg": 135 + }, + "dt_txt": "2025-06-14 12:00:00" + }, + { + "dt": 1718379600, + "main": { + "temp": 303.10, + "pressure": 1012, + "humidity": 48 + }, + "weather": [ + { + "description": "light rain", + "icon": "10d" + } + ], + "wind": { + "speed": 3.0, + "deg": 150 + }, + "dt_txt": "2025-06-14 15:00:00" + }, + { + "dt": 1718390400, + "main": { + "temp": 300.00, + "pressure": 1011, + "humidity": 55 + }, + "weather": [ + { + "description": "few clouds", + "icon": "02d" + } + ], + "wind": { + "speed": 2.2, + "deg": 160 + }, + "dt_txt": "2025-06-14 18:00:00" + }, + { + "dt": 1718401200, + "main": { + "temp": 298.50, + "pressure": 1010, + "humidity": 60 + }, + "weather": [ + { + "description": "clear sky", + "icon": "01n" + } + ], + "wind": { + "speed": 1.7, + "deg": 180 + }, + "dt_txt": "2025-06-14 21:00:00" + } + ] +} diff --git a/app/src/main/java/com/example/airquality/AQActivity.java b/app/src/main/java/com/example/airquality/AQActivity.java new file mode 100644 index 0000000..e80b2e9 --- /dev/null +++ b/app/src/main/java/com/example/airquality/AQActivity.java @@ -0,0 +1,191 @@ +package com.example.airquality; + +import android.os.Bundle; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuInflater; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class AQActivity extends AppCompatActivity { + private static final Map cityCoordinates = new HashMap<>(); + static { + cityCoordinates.put("Subotica", new double[]{46.1, 19.6667}); + cityCoordinates.put("Novi Sad", new double[]{45.2517, 19.8369}); + cityCoordinates.put("Beograd", new double[]{44.804, 20.4651}); + cityCoordinates.put("San Francisko", new double[]{37.7749, -122.4194}); + cityCoordinates.put("Sidnej", new double[]{-33.8679, 151.2073}); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_aqactivity); + Toolbar toolbar = findViewById(R.id.toolbar); + + // Using toolbar as ActionBar + setSupportActionBar(toolbar); + + // Display application icon in the toolbar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowHomeEnabled(true); + getSupportActionBar().setLogo(R.drawable.weather_clear_symbolic); + getSupportActionBar().setDisplayUseLogoEnabled(true); + } + + loadAQ(); + } + @Override + protected void onResume() { + super.onResume(); + loadAQ(); + } + + public boolean onCreateOptionsMenu(android.view.Menu menu) { + MenuInflater inflater = getMenuInflater(); + + inflater.inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + + @Override + public boolean onOptionsItemSelected(android.view.MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_aq: + Intent intentAQ = new Intent(this, AQActivity.class); + startActivity(intentAQ); + return true; + case R.id.menu_settings: + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + private String getAQIDescription(int aqi) { + switch (aqi) { + case 1: return "Good"; + case 2: return "Fair"; + case 3: return "Moderate"; + case 4: return "Poor"; + case 5: return "Very Poor"; + default: return "Unknown"; + } + } + public void loadAQ(){ + String city = PreferencesManager.getCity(this); + double[] coords = cityCoordinates.get(city); + if (coords == null) { + Toast.makeText(this, "Unknown city: " + city, Toast.LENGTH_SHORT).show(); + return; + } + String apiKey = getString(R.string.weather_api_key); + String url = String.format( + "https://api.openweathermap.org/data/2.5/air_pollution?lat=%.6f&lon=%.6f&appid=%s", + coords[0], coords[1], apiKey + ); + + RequestQueue queue = Volley.newRequestQueue(this); + JsonObjectRequest request = new JsonObjectRequest( + Request.Method.GET, + url, + null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + // Get JSON fields + JSONObject listItem = response.getJSONArray("list").getJSONObject(0); + int aqi = listItem.getJSONObject("main").getInt("aqi"); + + View rootLayout = findViewById(R.id.main); + + int colorResId; + switch (aqi) { + case 1: + colorResId = R.color.aqi_good; + break; + case 2: + colorResId = R.color.aqi_fair; + break; + case 3: + colorResId = R.color.aqi_moderate; + break; + case 4: + colorResId = R.color.aqi_poor; + break; + case 5: + colorResId = R.color.aqi_very_poor; + break; + default: + colorResId = android.R.color.background_light; + } + + rootLayout.setBackgroundColor(getResources().getColor(colorResId)); + + JSONObject components = listItem.getJSONObject("components"); + double pm25 = components.getDouble("pm2_5"); + double pm10 = components.getDouble("pm10"); + double o3 = components.getDouble("o3"); + double co = components.getDouble("co"); + double no2 = components.getDouble("no2"); + + + TextView tvCity = findViewById(R.id.tvCity); + TextView tvAQ = findViewById(R.id.tvAQ); + TextView tvComponents = findViewById(R.id.tvComponents); + + + String city = PreferencesManager.getCity(AQActivity.this); + + + tvCity.setText("City: " + city); + tvAQ.setText("AQI Index: " + aqi + " (" + getAQIDescription(aqi) + ")"); + tvComponents.setText( + String.format("PM2.5: %.2f µg/m³\nPM10: %.2f µg/m³\nO₃: %.2f ppb\nCO: %.2f ppm\nNO₂: %.2f ppb", + pm25, pm10, o3, co, no2) + ); + + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(AQActivity.this, "Failed to parse air data", Toast.LENGTH_SHORT).show(); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + Toast.makeText(AQActivity.this, "Failed to load air pollution data", Toast.LENGTH_SHORT).show(); + error.printStackTrace(); + } + } + ); + + queue.add(request); + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/airquality/DetailsActivity.java b/app/src/main/java/com/example/airquality/DetailsActivity.java new file mode 100644 index 0000000..71cf85a --- /dev/null +++ b/app/src/main/java/com/example/airquality/DetailsActivity.java @@ -0,0 +1,20 @@ +package com.example.airquality; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class DetailsActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_details); + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/airquality/MainActivity.java b/app/src/main/java/com/example/airquality/MainActivity.java new file mode 100644 index 0000000..2b1acd3 --- /dev/null +++ b/app/src/main/java/com/example/airquality/MainActivity.java @@ -0,0 +1,194 @@ +package com.example.airquality; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.MenuInflater; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; + +import androidx.appcompat.widget.Toolbar; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.google.android.material.appbar.MaterialToolbar; + +import org.json.JSONObject; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MainActivity extends AppCompatActivity { + List prognoza; + + private ListView listView; + + private static final Map cityCoordinates = new HashMap<>(); + static { + cityCoordinates.put("Subotica", new double[]{46.1, 19.6667}); + cityCoordinates.put("Novi Sad", new double[]{45.2517, 19.8369}); + cityCoordinates.put("Beograd", new double[]{44.804, 20.4651}); + cityCoordinates.put("San Francisko", new double[]{37.7749, -122.4194}); + cityCoordinates.put("Sidnej", new double[]{-33.8679, 151.2073}); + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + + listView=findViewById(R.id.listView); + + // Assigning ID of the toolbar to a variable + Toolbar toolbar = findViewById(R.id.toolbar); + + // Using toolbar as ActionBar + setSupportActionBar(toolbar); + + // Display application icon in the toolbar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowHomeEnabled(true); + getSupportActionBar().setLogo(R.drawable.weather_clear_symbolic); + getSupportActionBar().setDisplayUseLogoEnabled(true); + } + + loadForecast(); + } + +// private void loadForecast() { +// String city = PreferencesManager.getCity(this); +// int days = PreferencesManager.getDays(this); +// String units = PreferencesManager.getUnits(this); +// +// Log.d("SETTINGS", "City: " + city + ", Days: " + days + ", Units: " + units); +// +// try { +// // Učitavanje lokalnog JSON fajla +// InputStream is = getAssets().open("forecast_sample_40.json"); +// int size = is.available(); +// byte[] buffer = new byte[size]; +// is.read(buffer); +// is.close(); +// +// String jsonString = new String(buffer, "UTF-8"); +// JSONObject jsonObject = new JSONObject(jsonString); +// +// // Računamo maksimalan broj vremenskih tačaka +// int maxPoints = days * 8; // jer ima 8 tačaka dnevno na svakih 3h +// +// // Parsiramo podatke +// List prognoza = Weather3hParser.parse(jsonObject, maxPoints); +// +// // Povezujemo adapter sa ListView-om +// Weather3hAdapter adapter = new Weather3hAdapter(this, prognoza); +// ListView listView = findViewById(R.id.listView); +// listView.setAdapter(adapter); +// +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + + + public boolean onCreateOptionsMenu(android.view.Menu menu) { + MenuInflater inflater = getMenuInflater(); + + inflater.inflate(R.menu.menu_main, menu); + return super.onCreateOptionsMenu(menu); + } + + + @Override + public boolean onOptionsItemSelected(android.view.MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_aq: + Intent intentAQ = new Intent(this, AQActivity.class); + startActivity(intentAQ); + return true; + case R.id.menu_settings: + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + protected void onResume() { + super.onResume(); + loadForecast(); + } + + + + + private void loadForecast() { + String city = PreferencesManager.getCity(this); + int days = PreferencesManager.getDays(this); + String units = PreferencesManager.getUnits(this); + + double[] coords = cityCoordinates.get(city); + if (coords == null) { + Toast.makeText(this, "Unknown city: " + city, Toast.LENGTH_SHORT).show(); + return; + } + + String apiKey = getString(R.string.weather_api_key); + String url = String.format( + "https://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&units=%s&appid=%s", + coords[0], coords[1], units, apiKey + ); + + RequestQueue queue = Volley.newRequestQueue(this); + + JsonObjectRequest request = new JsonObjectRequest( + Request.Method.GET, + url, + null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + prognoza = Weather3hParser.parse(response, days);; + Weather3hAdapter adapter = new Weather3hAdapter(MainActivity.this, prognoza); + ListView listView = findViewById(R.id.listView); + listView.setAdapter(adapter); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + error.printStackTrace(); + Toast.makeText(MainActivity.this, "Failed to load weather data", Toast.LENGTH_SHORT).show(); + } + } + ); + + queue.add(request); + } + + + + + + + +} + + diff --git a/app/src/main/java/com/example/airquality/PreferencesManager.java b/app/src/main/java/com/example/airquality/PreferencesManager.java new file mode 100644 index 0000000..f9838a0 --- /dev/null +++ b/app/src/main/java/com/example/airquality/PreferencesManager.java @@ -0,0 +1,36 @@ +package com.example.airquality; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PreferencesManager { + + private static final String PREFS_NAME = "MyPrefs"; + private static final String KEY_CITY = "city"; + private static final String KEY_DAYS = "days"; + private static final String KEY_UNITS = "units"; + + public static void savePreferences(Context context, String city, int days, String units) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(KEY_CITY, city); + editor.putInt(KEY_DAYS, days); + editor.putString(KEY_UNITS, units); + editor.apply(); + } + + public static String getCity(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getString(KEY_CITY, "Subotica"); // default: Subotica + } + + public static int getDays(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getInt(KEY_DAYS, 7); // default: 7 dana + } + + public static String getUnits(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getString(KEY_UNITS, "metric"); // default: metric + } +} diff --git a/app/src/main/java/com/example/airquality/SettingsActivity.java b/app/src/main/java/com/example/airquality/SettingsActivity.java new file mode 100644 index 0000000..fc2686b --- /dev/null +++ b/app/src/main/java/com/example/airquality/SettingsActivity.java @@ -0,0 +1,82 @@ +package com.example.airquality; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.RadioGroup; +import android.widget.Spinner; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class SettingsActivity extends AppCompatActivity { + Spinner spinnerCity; + RadioGroup radioGroupDays, radioGroupUnits; + CheckBox checkBoxRemember; + Button buttonSave; + + String[] cities = {"Subotica", "Novi Sad", "Beograd", "San Francisko", "Sidnej"}; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + spinnerCity = findViewById(R.id.spinnerCity); + radioGroupDays = findViewById(R.id.radioGroupDays); + radioGroupUnits = findViewById(R.id.radioGroupUnits); + checkBoxRemember = findViewById(R.id.checkBoxRemember); + buttonSave = findViewById(R.id.buttonSave); + + // Podesi spinner + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, cities); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCity.setAdapter(adapter); + + buttonSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String selectedCity = spinnerCity.getSelectedItem().toString(); + + int days = 7; // podrazumevano + if (radioGroupDays.getCheckedRadioButtonId() == R.id.radio3days) { + days = 3; + } else if (radioGroupDays.getCheckedRadioButtonId() == R.id.radio5days) { + days = 5; + } + + String units = "metric"; + if (radioGroupUnits.getCheckedRadioButtonId() == R.id.radioImperial) { + units = "imperial"; + } + + boolean remember = checkBoxRemember.isChecked(); + + if (remember) { + SharedPreferences prefs = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("city", selectedCity); + editor.putInt("days", days); + editor.putString("units", units); + editor.apply(); + } + + Intent resultIntent = new Intent(); + resultIntent.putExtra("city", selectedCity); + resultIntent.putExtra("days", days); + resultIntent.putExtra("units", units); + setResult(RESULT_OK, resultIntent); + finish(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/airquality/Weather3h.java b/app/src/main/java/com/example/airquality/Weather3h.java new file mode 100644 index 0000000..72f1c21 --- /dev/null +++ b/app/src/main/java/com/example/airquality/Weather3h.java @@ -0,0 +1,57 @@ +package com.example.airquality; + + + public class Weather3h { + private String dateTime; + private double temp; + private int pressure; + private int humidity; + private double windSpeed; + private int windDeg; + private String description; + private String icon; + + public Weather3h(String dateTime, double temp, int pressure, int humidity, + double windSpeed, int windDeg, String description, String icon) { + this.dateTime = dateTime; + this.temp = temp; + this.pressure = pressure; + this.humidity = humidity; + this.windSpeed = windSpeed; + this.windDeg = windDeg; + this.description = description; + this.icon = icon; + } + + public String getDateTime() { + return dateTime; + } + + public double getTemp() { + return temp; + } + + public int getPressure() { + return pressure; + } + + public int getHumidity() { + return humidity; + } + + public double getWindSpeed() { + return windSpeed; + } + + public int getWindDeg() { + return windDeg; + } + + public String getDescription() { + return description; + } + + public String getIcon() { + return icon; + } + } diff --git a/app/src/main/java/com/example/airquality/Weather3hAdapter.java b/app/src/main/java/com/example/airquality/Weather3hAdapter.java new file mode 100644 index 0000000..2e63882 --- /dev/null +++ b/app/src/main/java/com/example/airquality/Weather3hAdapter.java @@ -0,0 +1,54 @@ +package com.example.airquality; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.List; + +public class Weather3hAdapter extends BaseAdapter { + private Context context; + private List weatherList; + private LayoutInflater inflater; + + public Weather3hAdapter(Context context, List weatherList) { + this.context = context; + this.weatherList = weatherList; + this.inflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return weatherList.size(); + } + + @Override + public Object getItem(int position) { + return weatherList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View rowView = inflater.inflate(R.layout.item_weather3h, parent, false); + + TextView txtDate = rowView.findViewById(R.id.txtDate); + TextView txtTemp = rowView.findViewById(R.id.txtTemp); + TextView txtDesc = rowView.findViewById(R.id.txtDesc); + + Weather3h item = weatherList.get(position); + + txtDate.setText(item.getDateTime()); + txtTemp.setText(String.format("%.1f°C", item.getTemp())); + txtDesc.setText(item.getDescription()); + + return rowView; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/airquality/Weather3hParser.java b/app/src/main/java/com/example/airquality/Weather3hParser.java new file mode 100644 index 0000000..14446da --- /dev/null +++ b/app/src/main/java/com/example/airquality/Weather3hParser.java @@ -0,0 +1,44 @@ +package com.example.airquality; + +import org.json.JSONArray; +import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; + +public class Weather3hParser { + public static List parse(JSONObject jsonObject, int maxPoints) { + List forecastList = new ArrayList<>(); + + try { + JSONArray listArray = jsonObject.getJSONArray("list"); + + for (int i = 0; i < listArray.length() && i < maxPoints; i++) { + JSONObject item = listArray.getJSONObject(i); + + String dateTime = item.getString("dt_txt"); + + JSONObject main = item.getJSONObject("main"); + double temp = main.getDouble("temp"); + int pressure = main.getInt("pressure"); + int humidity = main.getInt("humidity"); + + JSONObject weather = item.getJSONArray("weather").getJSONObject(0); + String description = weather.getString("description"); + String icon = weather.getString("icon"); + + JSONObject wind = item.getJSONObject("wind"); + double windSpeed = wind.getDouble("speed"); + int windDeg = wind.getInt("deg"); + + Weather3h weather3h = new Weather3h(dateTime, temp, pressure, humidity, windSpeed, windDeg, description, icon); + forecastList.add(weather3h); + } + + } catch (Exception e) { + e.printStackTrace(); + } + + return forecastList; + } +} + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_weather.png b/app/src/main/res/drawable/ic_weather.png new file mode 100644 index 0000000..558b3cb Binary files /dev/null and b/app/src/main/res/drawable/ic_weather.png differ diff --git a/app/src/main/res/drawable/spinner_background.xml b/app/src/main/res/drawable/spinner_background.xml new file mode 100644 index 0000000..898ef10 --- /dev/null +++ b/app/src/main/res/drawable/spinner_background.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/weather_clear_symbolic.xml b/app/src/main/res/drawable/weather_clear_symbolic.xml new file mode 100644 index 0000000..3870715 --- /dev/null +++ b/app/src/main/res/drawable/weather_clear_symbolic.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_aqactivity.xml b/app/src/main/res/layout/activity_aqactivity.xml new file mode 100644 index 0000000..4f2bbb4 --- /dev/null +++ b/app/src/main/res/layout/activity_aqactivity.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml new file mode 100644 index 0000000..2b0d73f --- /dev/null +++ b/app/src/main/res/layout/activity_details.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..bdacf39 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..e19598d --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +