diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4858af9..95307a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + + + = Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); + channel.setDescription("Varsler for kalenderhendelser"); + manager.createNotificationChannel(channel); + } + + Intent openAppIntent = new Intent(context, MainActivity.class); + openAppIntent.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 + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java b/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java index 601a4a5..0878e49 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java @@ -38,7 +38,6 @@ public class CalendarFullFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - recyclerView = view.findViewById(R.id.recycler_full_calendar); progressBar = view.findViewById(R.id.loading_full_calendar); emptyView = view.findViewById(R.id.empty_view_calendar); @@ -46,7 +45,6 @@ public class CalendarFullFragment extends Fragment { layoutManager = new LinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager); - backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); fetchAllEvents(); @@ -54,22 +52,21 @@ public class CalendarFullFragment extends Fragment { private void fetchAllEvents() { progressBar.setVisibility(View.VISIBLE); - // Hent personlige hendelser (Nå med historikk) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { + // NYTT: Hent fra Google direkte + String url = CalendarManager.getGoogleCalendarUrl(); + RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(Call call, Response response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); - apiEvents.add(e); - } + // Konverter og formatér + apiEvents = CalendarManager.convertGoogleResponse(response.body()); } // Flett og vis @@ -94,7 +91,7 @@ public class CalendarFullFragment extends Fragment { } @Override - public void onFailure(Call> call, Throwable t) { + public void onFailure(Call call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java index 46133d6..2c5a5ba 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java @@ -15,15 +15,86 @@ import java.util.TimeZone; 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")); + String timeMin = dateFormat.format(new Date(oneYearAgo)); + + return "https://www.googleapis.com/calendar/v3/calendars/" + + GOOGLE_CALENDAR_ID + + "/events?key=" + GOOGLE_API_KEY + + "&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 + } + + // 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; + + for (GoogleCalendarModels.Item item : response.items) { + String title = item.summary != null ? item.summary : "(Uten tittel)"; + String desc = item.description; + String loc = item.location; + + // Dato-logikk + String start = null; + String end = null; + + if (item.start != null) { + if (item.start.dateTime != null) start = item.start.dateTime; + else start = item.start.date; // Heldags + } + if (item.end != null) { + if (item.end.dateTime != null) end = item.end.dateTime; + else end = item.end.date; // Heldags + } + + // Varslings-logikk (Henter de sanne innstillingene) + 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 + } + } + } + } + + 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) --- + // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); - 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 ?"; @@ -38,7 +109,8 @@ public class CalendarManager { )) { if (cursor != null) { while (cursor.moveToNext()) { - ids.add(cursor.getString(0)); // Legg til kalender-ID + ids.add(cursor.getString(0)); + // Legg til kalender-ID } } } catch (Exception e) { @@ -49,7 +121,6 @@ public class CalendarManager { public static List getDeviceEvents(Context context) { List deviceEvents = new ArrayList<>(); - if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { return deviceEvents; @@ -57,7 +128,6 @@ public class CalendarManager { // 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; @@ -67,7 +137,6 @@ public class CalendarManager { 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, @@ -90,7 +159,6 @@ public class CalendarManager { } } selection.append(") AND "); - selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); @@ -98,7 +166,6 @@ 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, @@ -108,7 +175,6 @@ public class CalendarManager { )) { if (cursor != null) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - while (cursor.moveToNext()) { String title = cursor.getString(0); long dtStart = cursor.getLong(1); @@ -116,7 +182,6 @@ public class CalendarManager { String desc = cursor.getString(3); String loc = cursor.getString(4); int allDay = cursor.getInt(5); - String rawStart; String rawEnd; @@ -140,14 +205,11 @@ public class CalendarManager { return deviceEvents; } - // --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) --- 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")); @@ -157,7 +219,6 @@ 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); @@ -215,7 +276,6 @@ 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/GoogleCalendarModels.java b/app/src/main/java/com/kbs/kbsintranett/GoogleCalendarModels.java new file mode 100644 index 0000000..28bc501 --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/GoogleCalendarModels.java @@ -0,0 +1,59 @@ +package com.kbs.kbsintranett; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Hjelpeklasser for å parse JSON direkte fra Google Calendar API v3. + */ +public class GoogleCalendarModels { + + public static class Response { + @SerializedName("items") + public List items; + } + + public static class Item { + @SerializedName("summary") + public String summary; + + @SerializedName("description") + public String description; + + @SerializedName("location") + public String location; + + @SerializedName("start") + public TimePoint start; + + @SerializedName("end") + public TimePoint end; + + @SerializedName("reminders") + public Reminders reminders; + } + + public static class TimePoint { + @SerializedName("dateTime") + public String dateTime; // Format: 2025-12-15T10:00:00+01:00 + + @SerializedName("date") + public String date; // Format: 2025-12-15 (for heldags) + } + + public static class Reminders { + @SerializedName("useDefault") + public boolean useDefault; + + @SerializedName("overrides") + public List overrides; + } + + public static class Override { + @SerializedName("method") + public String method; // f.eks "popup" + + @SerializedName("minutes") + public int minutes; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index 56e729d..8e68992 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -37,7 +37,6 @@ public class HomeFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Håndter svar på kalendertillatelse requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), @@ -48,7 +47,6 @@ public class HomeFragment extends Fragment { } } ); - // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) startNotificationWorker(); } @@ -62,7 +60,6 @@ public class HomeFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // 0. Profil-knapp View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { @@ -74,7 +71,6 @@ public class HomeFragment extends Fragment { calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); // Sett tom adapter midlertidig calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); - // "Se alle" knapp for kalender TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); if (viewAllCalendar != null) { @@ -102,7 +98,6 @@ public class HomeFragment extends Fragment { newsRecycler.setNestedScrollingEnabled(false); // Sett tom adapter midlertidig newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); - // "Se alle" knapp for nyheter TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); if (viewAllNews != null) { @@ -118,26 +113,23 @@ public class HomeFragment extends Fragment { // 1. Hent personlige hendelser først (fra CalendarManager) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - // 2. Hent API-hendelser fra WordPress - WordPressApiService apiService = RetrofitClient.getApiService(); - apiService.getCalendarEvents().enqueue(new Callback>() { + // 2. Hent API-hendelser DIREKTE fra Google + String url = CalendarManager.getGoogleCalendarUrl(); + RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(Call call, Response response) { if (!isAdded()) return; List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); // Formatér datoer - apiEvents.add(e); - } + // Konverter fra Google-modell til KBS-modell + apiEvents = CalendarManager.convertGoogleResponse(response.body()); } // 3. Flett listene (API + Personlig) og sorter List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) - // (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende") List upcomingEvents = new ArrayList<>(); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); @@ -160,14 +152,12 @@ public class HomeFragment extends Fragment { } @Override - public void onFailure(Call> call, Throwable t) { + public void onFailure(Call call, Throwable t) { if (!isAdded()) return; // Hvis API feiler, vis bare personlige events hvis vi har noen if (!deviceEvents.isEmpty()) { List top5 = new ArrayList<>(); - // Filtrer og plukk topp 5 fra lokale også for(int i=0; i { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); sheet.show(getParentFragmentManager(), "CalendarDetails"); @@ -201,7 +191,8 @@ public class HomeFragment extends Fragment { for (WpPost post : wpPosts) { try { Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); // Setter pen dato + post.date = targetFormat.format(date); + // Setter pen dato } catch (Exception e) {} } diff --git a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java index cfd03b1..b940995 100644 --- a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java +++ b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java @@ -1,6 +1,13 @@ package com.kbs.kbsintranett; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.util.Log; import android.view.View; @@ -8,6 +15,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; @@ -32,6 +41,7 @@ public class MainActivity extends AppCompatActivity { 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); @@ -47,10 +57,50 @@ public class MainActivity extends AppCompatActivity { }); } + // --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) --- + checkExactAlarmPermission(); + // 2. Start Silent Sign-In ved oppstart checkLoginState(); } + /** + * 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()) { + // 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) { diff --git a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java index c0c5bf8..9b59010 100644 --- a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java +++ b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java @@ -1,12 +1,12 @@ package com.kbs.kbsintranett; -import android.app.NotificationChannel; -import android.app.NotificationManager; +import android.app.AlarmManager; +import android.app.PendingIntent; import android.content.Context; -import android.content.SharedPreferences; +import android.content.Intent; import android.os.Build; +import android.util.Log; import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; import androidx.work.Worker; import androidx.work.WorkerParameters; import java.io.IOException; @@ -18,8 +18,7 @@ import retrofit2.Response; public class NotificationWorker extends Worker { - private static final String CHANNEL_ID = "kbs_calendar_channel"; - private static final String PREFS_NAME = "KBSNotificationPrefs"; + private static final String TAG = "KBS_DEBUG"; public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); @@ -28,72 +27,108 @@ public class NotificationWorker extends Worker { @NonNull @Override public Result doWork() { - // Dette kjører i bakgrunnen + Log.d(TAG, "NotificationWorker: Starter sjekk av kalender..."); + try { - // Hent events synkront (ikke enqueue) - Response> response = RetrofitClient.getApiService().getCalendarEvents().execute(); + String url = CalendarManager.getGoogleCalendarUrl(); + // Hent events synkront + Response response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute(); if (response.isSuccessful() && response.body() != null) { - checkAndNotify(response.body()); + List events = CalendarManager.convertGoogleResponse(response.body()); + scheduleAlarms(events); return Result.success(); } else { + Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code()); return Result.retry(); } } catch (IOException e) { + Log.e(TAG, "NotificationWorker: Nettverksfeil", e); return Result.retry(); } } - private void checkAndNotify(List events) { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - long now = System.currentTimeMillis(); - long fifteenMinutes = 15 * 60 * 1000; + private void scheduleAlarms(List events) { + 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."); + 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()); - SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + + int countSet = 0; for (CalendarEvent event : events) { try { - Date eventDate; - if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); - else eventDate = sqlFormat.parse(event.getRawDate()); + // 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); + } if (eventDate == null) continue; - long diff = eventDate.getTime() - now; + // Beregn når alarmen skal gå + long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L); - // Hvis eventet starter innen de neste 30 min, og ikke allerede varslet - if (diff > 0 && diff < (30 * 60 * 1000)) { - String eventId = event.getTitle() + event.getRawDate(); // Enkel ID - boolean alreadyNotified = prefs.getBoolean(eventId, false); + // 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) + if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) { - if (!alreadyNotified) { - sendNotification(event.getTitle(), "Starter kl " + event.getTime()); - // Lagre at vi har varslet - prefs.edit().putBoolean(eventId, true).apply(); + // Lag en unik ID for alarmen + String uniqueIdString = event.getTitle() + event.getRawDate(); + int alarmId = uniqueIdString.hashCode(); + + Intent intent = new Intent(context, AlarmReceiver.class); + intent.putExtra("TITLE", event.getTitle()); + intent.putExtra("MESSAGE", "Starter kl " + event.getTime()); + intent.putExtra("ID", alarmId); + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, + alarmId, + intent, + 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)); + countSet++; } + } catch (Exception e) { - e.printStackTrace(); + Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e); } } - } - private void sendNotification(String title, String content) { - NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); - manager.createNotificationChannel(channel); + if (countSet == 0) { + Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå."); } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her - .setContentTitle(title) - .setContentText(content) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setAutoCancel(true); - - manager.notify((int) System.currentTimeMillis(), builder.build()); } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java index c4e6b51..b380da3 100644 --- a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java +++ b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java @@ -15,6 +15,7 @@ import retrofit2.http.Multipart; import retrofit2.http.Part; import retrofit2.http.PartMap; import retrofit2.http.Query; +import retrofit2.http.Url; // NY IMPORT public interface WordPressApiService { @GET("wp-json/wp/v2/posts?per_page=10&_embed") @@ -40,9 +41,14 @@ public interface WordPressApiService { @Part List files ); + // ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); + // NY: Direkte kall mot Google (bruker @Url for å override base URL) + @GET + Call getDirectGoogleEvents(@Url String fullUrl); + @GET("wp-json/gf/v2/entries") Call getEntries( @Query("form_ids") int formId, diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index 7c5359e..746c51f 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -177,6 +177,7 @@ FILSTI: app\src\main\AndroidManifest.xml + + + +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java +============================================================ +package com.kbs.kbsintranett; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; +import androidx.core.app.NotificationCompat; + +public class AlarmReceiver extends BroadcastReceiver { + + private static final String TAG = "KBS_DEBUG"; + private static final String CHANNEL_ID = "kbs_calendar_channel"; + + @Override + public void onReceive(Context context, Intent intent) { + String title = intent.getStringExtra("TITLE"); + String message = intent.getStringExtra("MESSAGE"); + int notificationId = intent.getIntExtra("ID", 0); + + Log.i(TAG, "AlarmReceiver: WAKE UP! Mottok alarm for: " + title); + + showNotification(context, title, message, notificationId); + } + + private void showNotification(Context context, String title, String message, int notificationId) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); + channel.setDescription("Varsler for kalenderhendelser"); + manager.createNotificationChannel(channel); + } + + Intent openAppIntent = new Intent(context, MainActivity.class); + openAppIntent.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 + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(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); + } + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java ============================================================ @@ -568,7 +640,6 @@ public class CalendarFullFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - recyclerView = view.findViewById(R.id.recycler_full_calendar); progressBar = view.findViewById(R.id.loading_full_calendar); emptyView = view.findViewById(R.id.empty_view_calendar); @@ -576,7 +647,6 @@ public class CalendarFullFragment extends Fragment { layoutManager = new LinearLayoutManager(getContext()); recyclerView.setLayoutManager(layoutManager); - backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); fetchAllEvents(); @@ -584,22 +654,21 @@ public class CalendarFullFragment extends Fragment { private void fetchAllEvents() { progressBar.setVisibility(View.VISIBLE); - // Hent personlige hendelser (Nå med historikk) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { + // NYTT: Hent fra Google direkte + String url = CalendarManager.getGoogleCalendarUrl(); + RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(Call call, Response response) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); - apiEvents.add(e); - } + // Konverter og formatér + apiEvents = CalendarManager.convertGoogleResponse(response.body()); } // Flett og vis @@ -624,7 +693,7 @@ public class CalendarFullFragment extends Fragment { } @Override - public void onFailure(Call> call, Throwable t) { + public void onFailure(Call call, Throwable t) { if (!isAdded()) return; progressBar.setVisibility(View.GONE); @@ -684,15 +753,86 @@ import java.util.TimeZone; 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")); + String timeMin = dateFormat.format(new Date(oneYearAgo)); + + return "https://www.googleapis.com/calendar/v3/calendars/" + + GOOGLE_CALENDAR_ID + + "/events?key=" + GOOGLE_API_KEY + + "&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 + } + + // 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; + + for (GoogleCalendarModels.Item item : response.items) { + String title = item.summary != null ? item.summary : "(Uten tittel)"; + String desc = item.description; + String loc = item.location; + + // Dato-logikk + String start = null; + String end = null; + + if (item.start != null) { + if (item.start.dateTime != null) start = item.start.dateTime; + else start = item.start.date; // Heldags + } + if (item.end != null) { + if (item.end.dateTime != null) end = item.end.dateTime; + else end = item.end.date; // Heldags + } + + // Varslings-logikk (Henter de sanne innstillingene) + 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 + } + } + } + } + + 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) --- + // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no private static List getKbsCalendarIds(Context context) { List ids = new ArrayList<>(); - 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 ?"; @@ -707,7 +847,8 @@ public class CalendarManager { )) { if (cursor != null) { while (cursor.moveToNext()) { - ids.add(cursor.getString(0)); // Legg til kalender-ID + ids.add(cursor.getString(0)); + // Legg til kalender-ID } } } catch (Exception e) { @@ -718,7 +859,6 @@ public class CalendarManager { public static List getDeviceEvents(Context context) { List deviceEvents = new ArrayList<>(); - if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { return deviceEvents; @@ -726,7 +866,6 @@ public class CalendarManager { // 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; @@ -736,7 +875,6 @@ public class CalendarManager { 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, @@ -759,7 +897,6 @@ public class CalendarManager { } } selection.append(") AND "); - selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); @@ -767,7 +904,6 @@ 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, @@ -777,7 +913,6 @@ public class CalendarManager { )) { if (cursor != null) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); - while (cursor.moveToNext()) { String title = cursor.getString(0); long dtStart = cursor.getLong(1); @@ -785,7 +920,6 @@ public class CalendarManager { String desc = cursor.getString(3); String loc = cursor.getString(4); int allDay = cursor.getInt(5); - String rawStart; String rawEnd; @@ -809,14 +943,11 @@ public class CalendarManager { return deviceEvents; } - // --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) --- 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")); @@ -826,7 +957,6 @@ 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); @@ -884,7 +1014,6 @@ public class CalendarManager { String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; return d1.compareTo(d2); }); - return all; } } @@ -2946,6 +3075,69 @@ public class FormSubmission { } } +============================================================ +FILSTI: app\src\main\java\com\kbs\kbsintranett\GoogleCalendarModels.java +============================================================ +package com.kbs.kbsintranett; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Hjelpeklasser for å parse JSON direkte fra Google Calendar API v3. + */ +public class GoogleCalendarModels { + + public static class Response { + @SerializedName("items") + public List items; + } + + public static class Item { + @SerializedName("summary") + public String summary; + + @SerializedName("description") + public String description; + + @SerializedName("location") + public String location; + + @SerializedName("start") + public TimePoint start; + + @SerializedName("end") + public TimePoint end; + + @SerializedName("reminders") + public Reminders reminders; + } + + public static class TimePoint { + @SerializedName("dateTime") + public String dateTime; // Format: 2025-12-15T10:00:00+01:00 + + @SerializedName("date") + public String date; // Format: 2025-12-15 (for heldags) + } + + public static class Reminders { + @SerializedName("useDefault") + public boolean useDefault; + + @SerializedName("overrides") + public List overrides; + } + + public static class Override { + @SerializedName("method") + public String method; // f.eks "popup" + + @SerializedName("minutes") + public int minutes; + } +} + ============================================================ FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java ============================================================ @@ -3767,7 +3959,6 @@ public class HomeFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Håndter svar på kalendertillatelse requestPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), @@ -3778,7 +3969,6 @@ public class HomeFragment extends Fragment { } } ); - // Start bakgrunnsjobb for varsling (kjører hver 15. minutt) startNotificationWorker(); } @@ -3792,7 +3982,6 @@ public class HomeFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // 0. Profil-knapp View profileBtn = view.findViewById(R.id.btn_profile); if (profileBtn != null) { @@ -3804,7 +3993,6 @@ public class HomeFragment extends Fragment { calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); // Sett tom adapter midlertidig calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); - // "Se alle" knapp for kalender TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); if (viewAllCalendar != null) { @@ -3832,7 +4020,6 @@ public class HomeFragment extends Fragment { newsRecycler.setNestedScrollingEnabled(false); // Sett tom adapter midlertidig newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); - // "Se alle" knapp for nyheter TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); if (viewAllNews != null) { @@ -3848,26 +4035,23 @@ public class HomeFragment extends Fragment { // 1. Hent personlige hendelser først (fra CalendarManager) List deviceEvents = CalendarManager.getDeviceEvents(getContext()); - // 2. Hent API-hendelser fra WordPress - WordPressApiService apiService = RetrofitClient.getApiService(); - apiService.getCalendarEvents().enqueue(new Callback>() { + // 2. Hent API-hendelser DIREKTE fra Google + String url = CalendarManager.getGoogleCalendarUrl(); + RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback() { @Override - public void onResponse(Call> call, Response> response) { + public void onResponse(Call call, Response response) { if (!isAdded()) return; List apiEvents = new ArrayList<>(); if (response.isSuccessful() && response.body() != null) { - for (CalendarEvent e : response.body()) { - CalendarManager.formatEventForUI(e); // Formatér datoer - apiEvents.add(e); - } + // Konverter fra Google-modell til KBS-modell + apiEvents = CalendarManager.convertGoogleResponse(response.body()); } // 3. Flett listene (API + Personlig) og sorter List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) - // (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende") List upcomingEvents = new ArrayList<>(); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); @@ -3890,14 +4074,12 @@ public class HomeFragment extends Fragment { } @Override - public void onFailure(Call> call, Throwable t) { + public void onFailure(Call call, Throwable t) { if (!isAdded()) return; // Hvis API feiler, vis bare personlige events hvis vi har noen if (!deviceEvents.isEmpty()) { List top5 = new ArrayList<>(); - // Filtrer og plukk topp 5 fra lokale også for(int i=0; i { CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); sheet.show(getParentFragmentManager(), "CalendarDetails"); @@ -3931,7 +4113,8 @@ public class HomeFragment extends Fragment { for (WpPost post : wpPosts) { try { Date date = rawFormat.parse(post.date); - post.date = targetFormat.format(date); // Setter pen dato + post.date = targetFormat.format(date); + // Setter pen dato } catch (Exception e) {} } @@ -4314,7 +4497,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java ============================================================ package com.kbs.kbsintranett; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.provider.Settings; import android.util.Log; import android.view.View; @@ -4322,6 +4512,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.navigation.NavController; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; @@ -4346,6 +4538,7 @@ public class MainActivity extends AppCompatActivity { 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); @@ -4361,10 +4554,50 @@ public class MainActivity extends AppCompatActivity { }); } + // --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) --- + checkExactAlarmPermission(); + // 2. Start Silent Sign-In ved oppstart checkLoginState(); } + /** + * 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()) { + // 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) { @@ -4780,13 +5013,13 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationWorker.java ============================================================ package com.kbs.kbsintranett; -import android.app.NotificationChannel; -import android.app.NotificationManager; +import android.app.AlarmManager; +import android.app.PendingIntent; import android.content.Context; -import android.content.SharedPreferences; +import android.content.Intent; import android.os.Build; +import android.util.Log; import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; import androidx.work.Worker; import androidx.work.WorkerParameters; import java.io.IOException; @@ -4798,8 +5031,7 @@ import retrofit2.Response; public class NotificationWorker extends Worker { - private static final String CHANNEL_ID = "kbs_calendar_channel"; - private static final String PREFS_NAME = "KBSNotificationPrefs"; + private static final String TAG = "KBS_DEBUG"; public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); @@ -4808,73 +5040,109 @@ public class NotificationWorker extends Worker { @NonNull @Override public Result doWork() { - // Dette kjører i bakgrunnen + Log.d(TAG, "NotificationWorker: Starter sjekk av kalender..."); + try { - // Hent events synkront (ikke enqueue) - Response> response = RetrofitClient.getApiService().getCalendarEvents().execute(); + String url = CalendarManager.getGoogleCalendarUrl(); + // Hent events synkront + Response response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute(); if (response.isSuccessful() && response.body() != null) { - checkAndNotify(response.body()); + List events = CalendarManager.convertGoogleResponse(response.body()); + scheduleAlarms(events); return Result.success(); } else { + Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code()); return Result.retry(); } } catch (IOException e) { + Log.e(TAG, "NotificationWorker: Nettverksfeil", e); return Result.retry(); } } - private void checkAndNotify(List events) { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - long now = System.currentTimeMillis(); - long fifteenMinutes = 15 * 60 * 1000; + private void scheduleAlarms(List events) { + 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."); + 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()); - SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + + int countSet = 0; for (CalendarEvent event : events) { try { - Date eventDate; - if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); - else eventDate = sqlFormat.parse(event.getRawDate()); + // 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); + } if (eventDate == null) continue; - long diff = eventDate.getTime() - now; + // Beregn når alarmen skal gå + long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L); - // Hvis eventet starter innen de neste 30 min, og ikke allerede varslet - if (diff > 0 && diff < (30 * 60 * 1000)) { - String eventId = event.getTitle() + event.getRawDate(); // Enkel ID - boolean alreadyNotified = prefs.getBoolean(eventId, false); + // 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) + if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) { - if (!alreadyNotified) { - sendNotification(event.getTitle(), "Starter kl " + event.getTime()); - // Lagre at vi har varslet - prefs.edit().putBoolean(eventId, true).apply(); + // Lag en unik ID for alarmen + String uniqueIdString = event.getTitle() + event.getRawDate(); + int alarmId = uniqueIdString.hashCode(); + + Intent intent = new Intent(context, AlarmReceiver.class); + intent.putExtra("TITLE", event.getTitle()); + intent.putExtra("MESSAGE", "Starter kl " + event.getTime()); + intent.putExtra("ID", alarmId); + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, + alarmId, + intent, + 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)); + countSet++; } + } catch (Exception e) { - e.printStackTrace(); + Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e); } } - } - private void sendNotification(String title, String content) { - NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH); - manager.createNotificationChannel(channel); + if (countSet == 0) { + Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå."); } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her - .setContentTitle(title) - .setContentText(content) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setAutoCancel(true); - - manager.notify((int) System.currentTimeMillis(), builder.build()); } } @@ -5247,6 +5515,7 @@ import retrofit2.http.Multipart; import retrofit2.http.Part; import retrofit2.http.PartMap; import retrofit2.http.Query; +import retrofit2.http.Url; // NY IMPORT public interface WordPressApiService { @GET("wp-json/wp/v2/posts?per_page=10&_embed") @@ -5272,9 +5541,14 @@ public interface WordPressApiService { @Part List files ); + // ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); + // NY: Direkte kall mot Google (bruker @Url for å override base URL) + @GET + Call getDirectGoogleEvents(@Url String fullUrl); + @GET("wp-json/gf/v2/entries") Call getEntries( @Query("form_ids") int formId,