diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b6bc734..5e26931 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,6 +16,11 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + // NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden + buildFeatures { + buildConfig = true + } + buildTypes { release { isMinifyEnabled = false @@ -26,7 +31,6 @@ android { } } compileOptions { - // ENDRET: Oppgradert til Java 11 for å fikse build warnings sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95307a6..93a976a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,11 +11,15 @@ + + + + android:exported="true" + android:theme="@style/Theme.KBSIntranett"> @@ -39,7 +44,10 @@ - + = Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); - channel.setDescription("Varsler for kalenderhendelser"); - manager.createNotificationChannel(channel); + // Sjekk rettigheter for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + // Vi kan ikke vise varsel uten rettighet. + return; + } } - Intent openAppIntent = new Intent(context, MainActivity.class); - openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + // Intent for hva som skjer når man trykker på varselet (åpne appen) + Intent tapIntent = new Intent(context, MainActivity.class); + tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( context, 0, - openAppIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + tapIntent, + PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.ic_launcher_foreground) // Pass på at du har et ikon her, ellers bruk R.mipmap.ic_launcher .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_EVENT) .setContentIntent(pendingIntent) .setAutoCancel(true); - try { - manager.notify(notificationId, builder.build()); - Log.d(TAG, "AlarmReceiver: Varsel sendt til systemet."); - } catch (SecurityException e) { - Log.e(TAG, "AlarmReceiver: Feil - Mangler tillatelse til å sende varsel!", e); - } catch (Exception e) { - Log.e(TAG, "AlarmReceiver: Ukjent feil ved visning av varsel", e); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(notificationId, builder.build()); + } + + private void createNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance); + channel.setDescription("Varsler for kalenderhendelser i KBS Intranett"); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java index 2c5a5ba..1398c51 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.provider.CalendarContract; +import android.util.Log; import androidx.core.content.ContextCompat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -17,12 +18,10 @@ public class CalendarManager { // --- KONFIGURASJON FOR GOOGLE DIREKTE-KOBLING --- private static final String GOOGLE_CALENDAR_ID = "kbservice.no_o2bmp5f9f540vedveit51optfo@group.calendar.google.com"; - // TODO: Sett inn din API-nøkkel her! private static final String GOOGLE_API_KEY = "AIzaSyCos8VW5mClUcuhs86gbSJo8uitY0fVPus"; public static String getGoogleCalendarUrl() { - // Hent hendelser fra 1 år tilbake i tid long oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -34,10 +33,9 @@ public class CalendarManager { + "&singleEvents=true" + "&orderBy=startTime" + "&maxResults=250" - + "&timeMin=" + timeMin; // URL-encoded er ikke nødvendig her siden Retrofit/OkHttp håndterer det, men timeMin bør være formatert + + "&timeMin=" + timeMin; } - // Konverterer Google Response til vår interne CalendarEvent public static List convertGoogleResponse(GoogleCalendarModels.Response response) { List events = new ArrayList<>(); if (response == null || response.items == null) return events; @@ -53,50 +51,69 @@ public class CalendarManager { if (item.start != null) { if (item.start.dateTime != null) start = item.start.dateTime; - else start = item.start.date; // Heldags + else start = item.start.date; } if (item.end != null) { if (item.end.dateTime != null) end = item.end.dateTime; - else end = item.end.date; // Heldags + else end = item.end.date; } - // Varslings-logikk (Henter de sanne innstillingene) + // --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) --- + if (title.toLowerCase().contains("test")) { + Log.d("KBS_DEBUG", "--------------------------------------------------"); + Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title); + Log.d("KBS_DEBUG", " Starttid: " + start); + + if (item.reminders != null) { + Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault); + + if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { + Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:"); + for (GoogleCalendarModels.Override override : item.reminders.overrides) { + Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes); + } + } else { + Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google."); + } + } else { + Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen."); + } + Log.d("KBS_DEBUG", "--------------------------------------------------"); + } + // ------------------------------------------------- + + // Varslings-logikk int reminderMinutes = 15; // Default fallback + if (item.reminders != null) { if (item.reminders.useDefault) { - reminderMinutes = 15; // Standard i Google er ofte 10 eller 15, vi antar 15 - } else if (item.reminders.overrides != null) { - for (GoogleCalendarModels.Override override : item.reminders.overrides) { - if ("popup".equalsIgnoreCase(override.method) || "alert".equalsIgnoreCase(override.method)) { - reminderMinutes = override.minutes; - break; // Bruk den første popup-varslingen vi finner - } - } + reminderMinutes = 15; + } else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { + // Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter + reminderMinutes = item.reminders.overrides.get(0).minutes; } } CalendarEvent event = new CalendarEvent(title, start, end, desc, loc); event.setReminderMinutes(reminderMinutes); - // Formatér for UI med en gang formatEventForUI(event); - events.add(event); } return events; } - // --- EKSISTERENDE KODE (UENDRET UNDER) --- + // --- RESTERENDE KODE (UENDRET) --- - // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { + return ids; + } String[] projection = new String[] { CalendarContract.Calendars._ID, CalendarContract.Calendars.ACCOUNT_NAME }; - // Vi ser etter kontoer som slutter på @kbs.no - // (SQL: account_name LIKE '%@kbs.no') String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; String[] selectionArgs = new String[] {"%@kbs.no"}; @@ -110,7 +127,6 @@ public class CalendarManager { if (cursor != null) { while (cursor.moveToNext()) { ids.add(cursor.getString(0)); - // Legg til kalender-ID } } } catch (Exception e) { @@ -126,17 +142,15 @@ public class CalendarManager { return deviceEvents; } - // 1. Finn først ID-ene til KBS-kalenderne List kbsCalendarIds = getKbsCalendarIds(context); - // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste if (kbsCalendarIds.isEmpty()) { return deviceEvents; } - // Hent events fra 1 år tilbake og 1 år frem long now = System.currentTimeMillis(); long startMillis = now - (365L * 24 * 60 * 60 * 1000); long endMillis = now + (365L * 24 * 60 * 60 * 1000); + String[] projection = new String[]{ CalendarContract.Events.TITLE, CalendarContract.Events.DTSTART, @@ -146,8 +160,6 @@ public class CalendarManager { CalendarContract.Events.ALL_DAY }; - // 2. Bygg opp spørringen for å filtrere på disse ID-ene - // Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?" StringBuilder selection = new StringBuilder("("); List selectionArgsList = new ArrayList<>(); @@ -166,6 +178,7 @@ public class CalendarManager { selectionArgsList.add(String.valueOf(endMillis)); String[] selectionArgs = selectionArgsList.toArray(new String[0]); + try (Cursor cursor = context.getContentResolver().query( CalendarContract.Events.CONTENT_URI, projection, @@ -182,6 +195,7 @@ public class CalendarManager { String desc = cursor.getString(3); String loc = cursor.getString(4); int allDay = cursor.getInt(5); + String rawStart; String rawEnd; @@ -207,9 +221,11 @@ public class CalendarManager { public static void formatEventForUI(CalendarEvent event) { if (event.getRawDate() == null) return; + SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); + outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); @@ -219,6 +235,7 @@ public class CalendarManager { boolean isAllDay = false; String raw = event.getRawDate(); + if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); date = shortFmt.parse(raw); @@ -276,6 +293,7 @@ public class CalendarManager { String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; return d1.compareTo(d2); }); + return all; } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/KbsApplication.java b/app/src/main/java/com/kbs/kbsintranett/KbsApplication.java new file mode 100644 index 0000000..1879bdb --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/KbsApplication.java @@ -0,0 +1,35 @@ +package com.kbs.kbsintranett; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; + +public class KbsApplication extends Application { + + public static final String CHANNEL_ID = "kbs_calendar_channel"; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + } + + private void createNotificationChannel() { + // Vi oppretter kanalen her ved oppstart, så den er klar uansett om + // det er MainActivity eller en bakgrunnsjobb som trenger den. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "KBS Kalendervarsler", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Varsler for kalenderhendelser"); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java index b940995..159d2a0 100644 --- a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java +++ b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java @@ -1,9 +1,13 @@ package com.kbs.kbsintranett; +import android.Manifest; import android.app.AlarmManager; import android.app.AlertDialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -11,11 +15,15 @@ import android.provider.Settings; import android.util.Log; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; -import androidx.work.OneTimeWorkRequest; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import com.google.android.gms.auth.api.signin.GoogleSignIn; @@ -24,31 +32,42 @@ import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.material.bottomnavigation.BottomNavigationView; +import java.util.concurrent.TimeUnit; + public class MainActivity extends AppCompatActivity { - // VIKTIG: Erstatt denne med din Web Client ID + // VIKTIG: Sørg for at denne matcher den du har i Google Cloud Console public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com"; + private static final String TAG = "MainActivity"; private NavController navController; private BottomNavigationView bottomNav; + // Launcher for å spørre om varslingstillatelse (Android 13+) + private ActivityResultLauncher requestPermissionLauncher; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - // 1. Setup UI + // --- 1. SETUP UI & NAVIGASJON --- + // Sjekket activity_main.xml: ID er "bottom_nav_view" bottomNav = findViewById(R.id.bottom_nav_view); + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); if (navHostFragment != null) { navController = navHostFragment.getNavController(); - NavigationUI.setupWithNavController(bottomNav, navController); + if (bottomNav != null) { + NavigationUI.setupWithNavController(bottomNav, navController); + } // Skjul meny på login-skjerm navController.addOnDestinationChangedListener((controller, destination, arguments) -> { - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet + if (bottomNav == null) return; + if (destination.getId() == R.id.navigation_login) { bottomNav.setVisibility(View.GONE); } else { @@ -57,50 +76,32 @@ public class MainActivity extends AppCompatActivity { }); } - // --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) --- + // --- 2. VARSLINGSOPPSETT (NYTT) --- + createNotificationChannel(); + + // Initialiser permission launcher for varsler + requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + Log.d(TAG, "Varslingstillatelse gitt!"); + } else { + Log.e(TAG, "Varslingstillatelse avslått. Bruker får ikke kalendervarsler."); + } + }); + + // Sjekk tillatelser (både Varsler og Alarmer) + checkNotificationPermission(); checkExactAlarmPermission(); - // 2. Start Silent Sign-In ved oppstart + // Start bakgrunnsjobben for kalenderen + scheduleCalendarWork(); + + // --- 3. AUTENTISERING (GAMMELT) --- checkLoginState(); } /** - * Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM). - * Hvis ikke, spør brukeren om å gå til innstillinger. + * Sjekker om brukeren er logget inn med Google, og gjør en silent refresh mot WP. */ - private void checkExactAlarmPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { - // Vi mangler tillatelse. Vis dialog. - new AlertDialog.Builder(this) - .setTitle("Varslingstillatelse kreves") - .setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.") - .setPositiveButton("Gå til Innstillinger", (dialog, which) -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - }) - .setNegativeButton("Senere", null) - .show(); - } else { - // Vi har tillatelse (eller er på eldre Android). Kjør logikk! - runNotificationWorker(); - } - } else { - // Eldre Android-versjoner trenger ikke denne tillatelsen - runNotificationWorker(); - } - } - - private void runNotificationWorker() { - // --- DEBUG: TVING KJØRING AV KALENDER-SJEKK --- - // Denne linjen kjører NotificationWorker umiddelbart ved oppstart for feilsøking. - // Fjern eller kommenter ut denne når testingen er ferdig. - WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.from(NotificationWorker.class)); - // ---------------------------------------------- - } - private void checkLoginState() { GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); if (account == null) { @@ -130,10 +131,8 @@ public class MainActivity extends AppCompatActivity { @Override public void onSuccess(String role) { Log.d(TAG, "Silent login fullført. Rolle: " + role); - // Gå videre til Home hvis vi står på Login if (navController != null && navController.getCurrentDestination() != null && navController.getCurrentDestination().getId() == R.id.navigation_login) { - // Denne aksjonen finnes i mobile_navigation.xml navController.navigate(R.id.action_login_to_home); } } @@ -155,11 +154,69 @@ public class MainActivity extends AppCompatActivity { private void navigateToLogin() { if (navController != null) { if (navController.getCurrentDestination() != null && - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet navController.getCurrentDestination().getId() != R.id.navigation_login) { - // Denne ID'en finnes i mobile_navigation.xml navController.navigate(R.id.navigation_login); } } } + + // --- NYE HJELPEMETODER FOR VARSLING --- + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "KBS Kalendervarsler"; + String description = "Varsler for kalenderhendelser"; + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", name, importance); + channel.setDescription(description); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } + + private void checkNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + } + + /** + * Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM). + * Hvis ikke, spør brukeren om å gå til innstillinger. + */ + private void checkExactAlarmPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { + new AlertDialog.Builder(this) + .setTitle("Varslingstillatelse kreves") + .setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.") + .setPositiveButton("Gå til Innstillinger", (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + }) + .setNegativeButton("Senere", null) + .show(); + } + } + } + + /** + * Starter WorkManager som sjekker kalenderen hvert 15. minutt. + */ + private void scheduleCalendarWork() { + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) + .build(); + + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "KbsCalendarWork", + ExistingPeriodicWorkPolicy.UPDATE, + workRequest + ); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java index 9b59010..df600de 100644 --- a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java +++ b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java @@ -31,7 +31,6 @@ public class NotificationWorker extends Worker { try { String url = CalendarManager.getGoogleCalendarUrl(); - // Hent events synkront Response response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute(); if (response.isSuccessful() && response.body() != null) { @@ -52,32 +51,23 @@ public class NotificationWorker extends Worker { Context context = getApplicationContext(); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - // Sjekk rettigheter (Android 12+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (!alarmManager.canScheduleExactAlarms()) { - Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse! Gå til Innstillinger -> Apper -> KBS -> Alarmer og påminnelser."); + Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!"); return; } } long now = System.currentTimeMillis(); - // Bruker en parser som er litt mer fleksibel for datoer SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - int countSet = 0; for (CalendarEvent event : events) { try { - // Hopp over heldagshendelser (dato uten klokkeslett) if (event.getRawDate().length() == 10) continue; Date eventDate = null; - // Enkel parsing. Merk: Google sender med tidssone (+01:00), - // men SimpleDateFormat uten 'X' vil parse dette som lokal tid hvis formatet stemmer. - // For optimal tidssone-håndtering burde vi brukt java.time (Android 8+), - // men dette fungerer greit så lenge telefonen er i samme sone som kalenderen. if (event.getRawDate().contains("T")) { - // Kutter vekk tidssone-offset for enkel parsing til lokal tid String raw = event.getRawDate(); if (raw.length() > 19) raw = raw.substring(0, 19); eventDate = isoFormat.parse(raw); @@ -85,14 +75,26 @@ public class NotificationWorker extends Worker { if (eventDate == null) continue; - // Beregn når alarmen skal gå long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L); - // Vi setter alarmen hvis tidspunktet er i fremtiden - // Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser) + // --- DETALJERT LOGGING FOR Å FEILSØKE --- + if (event.getTitle().toLowerCase().contains("test")) { + Log.d(TAG, "SJEKKER TEST-EVENT:"); + Log.d(TAG, " Start: " + eventDate); + Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før"); + Log.d(TAG, " Alarmtid: " + new Date(triggerTime)); + Log.d(TAG, " Nå: " + new Date(now)); + + if (triggerTime > now) { + Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)"); + } else { + Log.d(TAG, " Status: I FORTIDEN (Hoppes over)"); + } + } + // ---------------------------------------- + if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) { - // Lag en unik ID for alarmen String uniqueIdString = event.getTitle() + event.getRawDate(); int alarmId = uniqueIdString.hashCode(); @@ -108,17 +110,13 @@ public class NotificationWorker extends Worker { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); - // VIKTIG ENDRING: Vi setter alarmen PÅ NYTT hver gang. - // AlarmManager overskriver automatisk hvis ID er lik. - // Dette sikrer at alarmen faktisk ligger der, selv etter omstart av tlf. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } - Log.i(TAG, ">>> ALARM SATT (Oppdatert): " + event.getTitle() + " -> Skal ringe: " + new Date(triggerTime)); + Log.i(TAG, ">>> ALARM SATT: " + event.getTitle()); countSet++; } @@ -128,7 +126,7 @@ public class NotificationWorker extends Worker { } if (countSet == 0) { - Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå."); + Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t)."); } } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java index de6008e..c6676c1 100644 --- a/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java +++ b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java @@ -11,7 +11,7 @@ import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT +import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @@ -22,12 +22,16 @@ public class RetrofitClient { public static WordPressApiService getApiService() { if (retrofit == null) { - // NYTT: Logging Interceptor + // ENDRET: Redusert loggnivå fra BODY til BASIC for å unngå spam i Logcat HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); - logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers) + if (BuildConfig.DEBUG) { + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + } else { + logging.setLevel(HttpLoggingInterceptor.Level.NONE); + } OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(logging) // Legg til loggingen her + .addInterceptor(logging) .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { @@ -46,7 +50,7 @@ public class RetrofitClient { Gson gson = new GsonBuilder() .registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter()) - .setLenient() // NYTT: Gjør parsing litt mer tilgivende + .setLenient() .create(); retrofit = new Retrofit.Builder() diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index 746c51f..b4c730f 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -59,6 +59,11 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + // NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden + buildFeatures { + buildConfig = true + } + buildTypes { release { isMinifyEnabled = false @@ -69,7 +74,6 @@ android { } } compileOptions { - // ENDRET: Oppgradert til Java 11 for å fikse build warnings sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } @@ -175,11 +179,15 @@ FILSTI: app\src\main\AndroidManifest.xml + + + + android:exported="true" + android:theme="@style/Theme.KBSIntranett"> @@ -203,7 +212,10 @@ FILSTI: app\src\main\AndroidManifest.xml - + = Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); - channel.setDescription("Varsler for kalenderhendelser"); - manager.createNotificationChannel(channel); + // Sjekk rettigheter for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + // Vi kan ikke vise varsel uten rettighet. + return; + } } - Intent openAppIntent = new Intent(context, MainActivity.class); - openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + // Intent for hva som skjer når man trykker på varselet (åpne appen) + Intent tapIntent = new Intent(context, MainActivity.class); + tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( context, 0, - openAppIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + tapIntent, + PendingIntent.FLAG_IMMUTABLE ); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.ic_launcher_foreground) // Pass på at du har et ikon her, ellers bruk R.mipmap.ic_launcher .setContentTitle(title) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_EVENT) .setContentIntent(pendingIntent) .setAutoCancel(true); - try { - manager.notify(notificationId, builder.build()); - Log.d(TAG, "AlarmReceiver: Varsel sendt til systemet."); - } catch (SecurityException e) { - Log.e(TAG, "AlarmReceiver: Feil - Mangler tillatelse til å sende varsel!", e); - } catch (Exception e) { - Log.e(TAG, "AlarmReceiver: Ukjent feil ved visning av varsel", e); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(notificationId, builder.build()); + } + + private void createNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance); + channel.setDescription("Varsler for kalenderhendelser i KBS Intranett"); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } } } } @@ -742,6 +763,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.provider.CalendarContract; +import android.util.Log; import androidx.core.content.ContextCompat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -755,12 +777,10 @@ public class CalendarManager { // --- KONFIGURASJON FOR GOOGLE DIREKTE-KOBLING --- private static final String GOOGLE_CALENDAR_ID = "kbservice.no_o2bmp5f9f540vedveit51optfo@group.calendar.google.com"; - // TODO: Sett inn din API-nøkkel her! private static final String GOOGLE_API_KEY = "AIzaSyCos8VW5mClUcuhs86gbSJo8uitY0fVPus"; public static String getGoogleCalendarUrl() { - // Hent hendelser fra 1 år tilbake i tid long oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -772,10 +792,9 @@ public class CalendarManager { + "&singleEvents=true" + "&orderBy=startTime" + "&maxResults=250" - + "&timeMin=" + timeMin; // URL-encoded er ikke nødvendig her siden Retrofit/OkHttp håndterer det, men timeMin bør være formatert + + "&timeMin=" + timeMin; } - // Konverterer Google Response til vår interne CalendarEvent public static List convertGoogleResponse(GoogleCalendarModels.Response response) { List events = new ArrayList<>(); if (response == null || response.items == null) return events; @@ -791,50 +810,69 @@ public class CalendarManager { if (item.start != null) { if (item.start.dateTime != null) start = item.start.dateTime; - else start = item.start.date; // Heldags + else start = item.start.date; } if (item.end != null) { if (item.end.dateTime != null) end = item.end.dateTime; - else end = item.end.date; // Heldags + else end = item.end.date; } - // Varslings-logikk (Henter de sanne innstillingene) + // --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) --- + if (title.toLowerCase().contains("test")) { + Log.d("KBS_DEBUG", "--------------------------------------------------"); + Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title); + Log.d("KBS_DEBUG", " Starttid: " + start); + + if (item.reminders != null) { + Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault); + + if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { + Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:"); + for (GoogleCalendarModels.Override override : item.reminders.overrides) { + Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes); + } + } else { + Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google."); + } + } else { + Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen."); + } + Log.d("KBS_DEBUG", "--------------------------------------------------"); + } + // ------------------------------------------------- + + // Varslings-logikk int reminderMinutes = 15; // Default fallback + if (item.reminders != null) { if (item.reminders.useDefault) { - reminderMinutes = 15; // Standard i Google er ofte 10 eller 15, vi antar 15 - } else if (item.reminders.overrides != null) { - for (GoogleCalendarModels.Override override : item.reminders.overrides) { - if ("popup".equalsIgnoreCase(override.method) || "alert".equalsIgnoreCase(override.method)) { - reminderMinutes = override.minutes; - break; // Bruk den første popup-varslingen vi finner - } - } + reminderMinutes = 15; + } else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) { + // Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter + reminderMinutes = item.reminders.overrides.get(0).minutes; } } CalendarEvent event = new CalendarEvent(title, start, end, desc, loc); event.setReminderMinutes(reminderMinutes); - // Formatér for UI med en gang formatEventForUI(event); - events.add(event); } return events; } - // --- EKSISTERENDE KODE (UENDRET UNDER) --- + // --- RESTERENDE KODE (UENDRET) --- - // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { + return ids; + } String[] projection = new String[] { CalendarContract.Calendars._ID, CalendarContract.Calendars.ACCOUNT_NAME }; - // Vi ser etter kontoer som slutter på @kbs.no - // (SQL: account_name LIKE '%@kbs.no') String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; String[] selectionArgs = new String[] {"%@kbs.no"}; @@ -848,7 +886,6 @@ public class CalendarManager { if (cursor != null) { while (cursor.moveToNext()) { ids.add(cursor.getString(0)); - // Legg til kalender-ID } } } catch (Exception e) { @@ -864,17 +901,15 @@ public class CalendarManager { return deviceEvents; } - // 1. Finn først ID-ene til KBS-kalenderne List kbsCalendarIds = getKbsCalendarIds(context); - // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste if (kbsCalendarIds.isEmpty()) { return deviceEvents; } - // Hent events fra 1 år tilbake og 1 år frem long now = System.currentTimeMillis(); long startMillis = now - (365L * 24 * 60 * 60 * 1000); long endMillis = now + (365L * 24 * 60 * 60 * 1000); + String[] projection = new String[]{ CalendarContract.Events.TITLE, CalendarContract.Events.DTSTART, @@ -884,8 +919,6 @@ public class CalendarManager { CalendarContract.Events.ALL_DAY }; - // 2. Bygg opp spørringen for å filtrere på disse ID-ene - // Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?" StringBuilder selection = new StringBuilder("("); List selectionArgsList = new ArrayList<>(); @@ -904,6 +937,7 @@ public class CalendarManager { selectionArgsList.add(String.valueOf(endMillis)); String[] selectionArgs = selectionArgsList.toArray(new String[0]); + try (Cursor cursor = context.getContentResolver().query( CalendarContract.Events.CONTENT_URI, projection, @@ -920,6 +954,7 @@ public class CalendarManager { String desc = cursor.getString(3); String loc = cursor.getString(4); int allDay = cursor.getInt(5); + String rawStart; String rawEnd; @@ -945,9 +980,11 @@ public class CalendarManager { public static void formatEventForUI(CalendarEvent event) { if (event.getRawDate() == null) return; + SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); + outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); @@ -957,6 +994,7 @@ public class CalendarManager { boolean isAllDay = false; String raw = event.getRawDate(); + if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); date = shortFmt.parse(raw); @@ -1014,6 +1052,7 @@ public class CalendarManager { String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; return d1.compareTo(d2); }); + return all; } } @@ -4325,6 +4364,45 @@ public class InternalLinkMovementMethod extends LinkMovementMethod { } } +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\KbsApplication.java +============================================================ +package com.kbs.kbsintranett; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; + +public class KbsApplication extends Application { + + public static final String CHANNEL_ID = "kbs_calendar_channel"; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + } + + private void createNotificationChannel() { + // Vi oppretter kanalen her ved oppstart, så den er klar uansett om + // det er MainActivity eller en bakgrunnsjobb som trenger den. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "KBS Kalendervarsler", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Varsler for kalenderhendelser"); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginFragment.java ============================================================ @@ -4497,10 +4575,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java ============================================================ package com.kbs.kbsintranett; +import android.Manifest; import android.app.AlarmManager; import android.app.AlertDialog; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -4508,11 +4590,15 @@ import android.provider.Settings; import android.util.Log; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; -import androidx.work.OneTimeWorkRequest; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; import com.google.android.gms.auth.api.signin.GoogleSignIn; @@ -4521,31 +4607,42 @@ import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.material.bottomnavigation.BottomNavigationView; +import java.util.concurrent.TimeUnit; + public class MainActivity extends AppCompatActivity { - // VIKTIG: Erstatt denne med din Web Client ID + // VIKTIG: Sørg for at denne matcher den du har i Google Cloud Console public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com"; + private static final String TAG = "MainActivity"; private NavController navController; private BottomNavigationView bottomNav; + // Launcher for å spørre om varslingstillatelse (Android 13+) + private ActivityResultLauncher requestPermissionLauncher; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - // 1. Setup UI + // --- 1. SETUP UI & NAVIGASJON --- + // Sjekket activity_main.xml: ID er "bottom_nav_view" bottomNav = findViewById(R.id.bottom_nav_view); + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); if (navHostFragment != null) { navController = navHostFragment.getNavController(); - NavigationUI.setupWithNavController(bottomNav, navController); + if (bottomNav != null) { + NavigationUI.setupWithNavController(bottomNav, navController); + } // Skjul meny på login-skjerm navController.addOnDestinationChangedListener((controller, destination, arguments) -> { - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet + if (bottomNav == null) return; + if (destination.getId() == R.id.navigation_login) { bottomNav.setVisibility(View.GONE); } else { @@ -4554,50 +4651,32 @@ public class MainActivity extends AppCompatActivity { }); } - // --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) --- + // --- 2. VARSLINGSOPPSETT (NYTT) --- + createNotificationChannel(); + + // Initialiser permission launcher for varsler + requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + Log.d(TAG, "Varslingstillatelse gitt!"); + } else { + Log.e(TAG, "Varslingstillatelse avslått. Bruker får ikke kalendervarsler."); + } + }); + + // Sjekk tillatelser (både Varsler og Alarmer) + checkNotificationPermission(); checkExactAlarmPermission(); - // 2. Start Silent Sign-In ved oppstart + // Start bakgrunnsjobben for kalenderen + scheduleCalendarWork(); + + // --- 3. AUTENTISERING (GAMMELT) --- checkLoginState(); } /** - * Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM). - * Hvis ikke, spør brukeren om å gå til innstillinger. + * Sjekker om brukeren er logget inn med Google, og gjør en silent refresh mot WP. */ - private void checkExactAlarmPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { - // Vi mangler tillatelse. Vis dialog. - new AlertDialog.Builder(this) - .setTitle("Varslingstillatelse kreves") - .setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.") - .setPositiveButton("Gå til Innstillinger", (dialog, which) -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - }) - .setNegativeButton("Senere", null) - .show(); - } else { - // Vi har tillatelse (eller er på eldre Android). Kjør logikk! - runNotificationWorker(); - } - } else { - // Eldre Android-versjoner trenger ikke denne tillatelsen - runNotificationWorker(); - } - } - - private void runNotificationWorker() { - // --- DEBUG: TVING KJØRING AV KALENDER-SJEKK --- - // Denne linjen kjører NotificationWorker umiddelbart ved oppstart for feilsøking. - // Fjern eller kommenter ut denne når testingen er ferdig. - WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.from(NotificationWorker.class)); - // ---------------------------------------------- - } - private void checkLoginState() { GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); if (account == null) { @@ -4627,10 +4706,8 @@ public class MainActivity extends AppCompatActivity { @Override public void onSuccess(String role) { Log.d(TAG, "Silent login fullført. Rolle: " + role); - // Gå videre til Home hvis vi står på Login if (navController != null && navController.getCurrentDestination() != null && navController.getCurrentDestination().getId() == R.id.navigation_login) { - // Denne aksjonen finnes i mobile_navigation.xml navController.navigate(R.id.action_login_to_home); } } @@ -4652,13 +4729,71 @@ public class MainActivity extends AppCompatActivity { private void navigateToLogin() { if (navController != null) { if (navController.getCurrentDestination() != null && - // Sjekker mot R.id.navigation_login som er ID'en til fragmentet navController.getCurrentDestination().getId() != R.id.navigation_login) { - // Denne ID'en finnes i mobile_navigation.xml navController.navigate(R.id.navigation_login); } } } + + // --- NYE HJELPEMETODER FOR VARSLING --- + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "KBS Kalendervarsler"; + String description = "Varsler for kalenderhendelser"; + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", name, importance); + channel.setDescription(description); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } + + private void checkNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + } + + /** + * Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM). + * Hvis ikke, spør brukeren om å gå til innstillinger. + */ + private void checkExactAlarmPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) { + new AlertDialog.Builder(this) + .setTitle("Varslingstillatelse kreves") + .setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.") + .setPositiveButton("Gå til Innstillinger", (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + }) + .setNegativeButton("Senere", null) + .show(); + } + } + } + + /** + * Starter WorkManager som sjekker kalenderen hvert 15. minutt. + */ + private void scheduleCalendarWork() { + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) + .build(); + + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + "KbsCalendarWork", + ExistingPeriodicWorkPolicy.UPDATE, + workRequest + ); + } } ============================================================ @@ -5044,7 +5179,6 @@ public class NotificationWorker extends Worker { try { String url = CalendarManager.getGoogleCalendarUrl(); - // Hent events synkront Response response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute(); if (response.isSuccessful() && response.body() != null) { @@ -5065,32 +5199,23 @@ public class NotificationWorker extends Worker { Context context = getApplicationContext(); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - // Sjekk rettigheter (Android 12+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (!alarmManager.canScheduleExactAlarms()) { - Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse! Gå til Innstillinger -> Apper -> KBS -> Alarmer og påminnelser."); + Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!"); return; } } long now = System.currentTimeMillis(); - // Bruker en parser som er litt mer fleksibel for datoer SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - int countSet = 0; for (CalendarEvent event : events) { try { - // Hopp over heldagshendelser (dato uten klokkeslett) if (event.getRawDate().length() == 10) continue; Date eventDate = null; - // Enkel parsing. Merk: Google sender med tidssone (+01:00), - // men SimpleDateFormat uten 'X' vil parse dette som lokal tid hvis formatet stemmer. - // For optimal tidssone-håndtering burde vi brukt java.time (Android 8+), - // men dette fungerer greit så lenge telefonen er i samme sone som kalenderen. if (event.getRawDate().contains("T")) { - // Kutter vekk tidssone-offset for enkel parsing til lokal tid String raw = event.getRawDate(); if (raw.length() > 19) raw = raw.substring(0, 19); eventDate = isoFormat.parse(raw); @@ -5098,14 +5223,26 @@ public class NotificationWorker extends Worker { if (eventDate == null) continue; - // Beregn når alarmen skal gå long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L); - // Vi setter alarmen hvis tidspunktet er i fremtiden - // Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser) + // --- DETALJERT LOGGING FOR Å FEILSØKE --- + if (event.getTitle().toLowerCase().contains("test")) { + Log.d(TAG, "SJEKKER TEST-EVENT:"); + Log.d(TAG, " Start: " + eventDate); + Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før"); + Log.d(TAG, " Alarmtid: " + new Date(triggerTime)); + Log.d(TAG, " Nå: " + new Date(now)); + + if (triggerTime > now) { + Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)"); + } else { + Log.d(TAG, " Status: I FORTIDEN (Hoppes over)"); + } + } + // ---------------------------------------- + if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) { - // Lag en unik ID for alarmen String uniqueIdString = event.getTitle() + event.getRawDate(); int alarmId = uniqueIdString.hashCode(); @@ -5121,17 +5258,13 @@ public class NotificationWorker extends Worker { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); - // VIKTIG ENDRING: Vi setter alarmen PÅ NYTT hver gang. - // AlarmManager overskriver automatisk hvis ID er lik. - // Dette sikrer at alarmen faktisk ligger der, selv etter omstart av tlf. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } - Log.i(TAG, ">>> ALARM SATT (Oppdatert): " + event.getTitle() + " -> Skal ringe: " + new Date(triggerTime)); + Log.i(TAG, ">>> ALARM SATT: " + event.getTitle()); countSet++; } @@ -5141,7 +5274,7 @@ public class NotificationWorker extends Worker { } if (countSet == 0) { - Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå."); + Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t)."); } } } @@ -5260,7 +5393,7 @@ import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT +import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @@ -5271,12 +5404,16 @@ public class RetrofitClient { public static WordPressApiService getApiService() { if (retrofit == null) { - // NYTT: Logging Interceptor + // ENDRET: Redusert loggnivå fra BODY til BASIC for å unngå spam i Logcat HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); - logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers) + if (BuildConfig.DEBUG) { + logging.setLevel(HttpLoggingInterceptor.Level.BASIC); + } else { + logging.setLevel(HttpLoggingInterceptor.Level.NONE); + } OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(logging) // Legg til loggingen her + .addInterceptor(logging) .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { @@ -5295,7 +5432,7 @@ public class RetrofitClient { Gson gson = new GsonBuilder() .registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter()) - .setLenient() // NYTT: Gjør parsing litt mer tilgivende + .setLenient() .create(); retrofit = new Retrofit.Builder()