Før nytt forsøk på Google Kalender

This commit is contained in:
ErolHaagenrud 2025-12-15 17:31:44 +01:00
parent 29a36e8a61
commit d9d128245c
10 changed files with 714 additions and 174 deletions

View file

@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.READ_CALENDAR" /> <uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -38,6 +39,8 @@
<activity android:name=".WebViewActivity" /> <activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider" android:authorities="com.kbs.kbsintranett.fileprovider"

View file

@ -0,0 +1,65 @@
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);
}
}
}

View file

@ -38,7 +38,6 @@ public class CalendarFullFragment extends Fragment {
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_full_calendar); recyclerView = view.findViewById(R.id.recycler_full_calendar);
progressBar = view.findViewById(R.id.loading_full_calendar); progressBar = view.findViewById(R.id.loading_full_calendar);
emptyView = view.findViewById(R.id.empty_view_calendar); emptyView = view.findViewById(R.id.empty_view_calendar);
@ -46,7 +45,6 @@ public class CalendarFullFragment extends Fragment {
layoutManager = new LinearLayoutManager(getContext()); layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
fetchAllEvents(); fetchAllEvents();
@ -54,22 +52,21 @@ public class CalendarFullFragment extends Fragment {
private void fetchAllEvents() { private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
// Hent personlige hendelser ( med historikk) // Hent personlige hendelser ( med historikk)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext()); List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { // NYTT: Hent fra Google direkte
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override @Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return; if (!isAdded()) return;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>(); List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) { // Konverter og formatér
CalendarManager.formatEventForUI(e); apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents.add(e);
}
} }
// Flett og vis // Flett og vis
@ -94,7 +91,7 @@ public class CalendarFullFragment extends Fragment {
} }
@Override @Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) { public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return; if (!isAdded()) return;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);

View file

@ -15,15 +15,86 @@ import java.util.TimeZone;
public class CalendarManager { 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<CalendarEvent> convertGoogleResponse(GoogleCalendarModels.Response response) {
List<CalendarEvent> 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 // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) { private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();
String[] projection = new String[] { String[] projection = new String[] {
CalendarContract.Calendars._ID, CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME CalendarContract.Calendars.ACCOUNT_NAME
}; };
// Vi ser etter kontoer som slutter @kbs.no // Vi ser etter kontoer som slutter @kbs.no
// (SQL: account_name LIKE '%@kbs.no') // (SQL: account_name LIKE '%@kbs.no')
String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?";
@ -38,7 +109,8 @@ public class CalendarManager {
)) { )) {
if (cursor != null) { if (cursor != null) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
ids.add(cursor.getString(0)); // Legg til kalender-ID ids.add(cursor.getString(0));
// Legg til kalender-ID
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -49,7 +121,6 @@ public class CalendarManager {
public static List<CalendarEvent> getDeviceEvents(Context context) { public static List<CalendarEvent> getDeviceEvents(Context context) {
List<CalendarEvent> deviceEvents = new ArrayList<>(); List<CalendarEvent> deviceEvents = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
return deviceEvents; return deviceEvents;
@ -57,7 +128,6 @@ public class CalendarManager {
// 1. Finn først ID-ene til KBS-kalenderne // 1. Finn først ID-ene til KBS-kalenderne
List<String> kbsCalendarIds = getKbsCalendarIds(context); List<String> kbsCalendarIds = getKbsCalendarIds(context);
// Hvis ingen kbs-kalendere finnes telefonen, returner tom liste // Hvis ingen kbs-kalendere finnes telefonen, returner tom liste
if (kbsCalendarIds.isEmpty()) { if (kbsCalendarIds.isEmpty()) {
return deviceEvents; return deviceEvents;
@ -67,7 +137,6 @@ public class CalendarManager {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long startMillis = now - (365L * 24 * 60 * 60 * 1000); long startMillis = now - (365L * 24 * 60 * 60 * 1000);
long endMillis = now + (365L * 24 * 60 * 60 * 1000); long endMillis = now + (365L * 24 * 60 * 60 * 1000);
String[] projection = new String[]{ String[] projection = new String[]{
CalendarContract.Events.TITLE, CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART, CalendarContract.Events.DTSTART,
@ -90,7 +159,6 @@ public class CalendarManager {
} }
} }
selection.append(") AND "); selection.append(") AND ");
selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND ");
selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); selection.append(CalendarContract.Events.DTSTART).append(" <= ?");
@ -98,7 +166,6 @@ public class CalendarManager {
selectionArgsList.add(String.valueOf(endMillis)); selectionArgsList.add(String.valueOf(endMillis));
String[] selectionArgs = selectionArgsList.toArray(new String[0]); String[] selectionArgs = selectionArgsList.toArray(new String[0]);
try (Cursor cursor = context.getContentResolver().query( try (Cursor cursor = context.getContentResolver().query(
CalendarContract.Events.CONTENT_URI, CalendarContract.Events.CONTENT_URI,
projection, projection,
@ -108,7 +175,6 @@ public class CalendarManager {
)) { )) {
if (cursor != null) { if (cursor != null) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
String title = cursor.getString(0); String title = cursor.getString(0);
long dtStart = cursor.getLong(1); long dtStart = cursor.getLong(1);
@ -116,7 +182,6 @@ public class CalendarManager {
String desc = cursor.getString(3); String desc = cursor.getString(3);
String loc = cursor.getString(4); String loc = cursor.getString(4);
int allDay = cursor.getInt(5); int allDay = cursor.getInt(5);
String rawStart; String rawStart;
String rawEnd; String rawEnd;
@ -140,14 +205,11 @@ public class CalendarManager {
return deviceEvents; return deviceEvents;
} }
// --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) ---
public static void formatEventForUI(CalendarEvent event) { public static void formatEventForUI(CalendarEvent event) {
if (event.getRawDate() == null) return; if (event.getRawDate() == null) return;
SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault());
SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO"));
SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
@ -157,7 +219,6 @@ public class CalendarManager {
boolean isAllDay = false; boolean isAllDay = false;
String raw = event.getRawDate(); String raw = event.getRawDate();
if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) {
SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
date = shortFmt.parse(raw); date = shortFmt.parse(raw);
@ -215,7 +276,6 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2); return d1.compareTo(d2);
}); });
return all; return all;
} }
} }

View file

@ -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<Item> 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<Override> overrides;
}
public static class Override {
@SerializedName("method")
public String method; // f.eks "popup"
@SerializedName("minutes")
public int minutes;
}
}

View file

@ -37,7 +37,6 @@ public class HomeFragment extends Fragment {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Håndter svar kalendertillatelse // Håndter svar kalendertillatelse
requestPermissionLauncher = registerForActivityResult( requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(), new ActivityResultContracts.RequestPermission(),
@ -48,7 +47,6 @@ public class HomeFragment extends Fragment {
} }
} }
); );
// Start bakgrunnsjobb for varsling (kjører hver 15. minutt) // Start bakgrunnsjobb for varsling (kjører hver 15. minutt)
startNotificationWorker(); startNotificationWorker();
} }
@ -62,7 +60,6 @@ public class HomeFragment extends Fragment {
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// 0. Profil-knapp // 0. Profil-knapp
View profileBtn = view.findViewById(R.id.btn_profile); View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) { if (profileBtn != null) {
@ -74,7 +71,6 @@ public class HomeFragment extends Fragment {
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
// Sett tom adapter midlertidig // Sett tom adapter midlertidig
calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
// "Se alle" knapp for kalender // "Se alle" knapp for kalender
TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar);
if (viewAllCalendar != null) { if (viewAllCalendar != null) {
@ -102,7 +98,6 @@ public class HomeFragment extends Fragment {
newsRecycler.setNestedScrollingEnabled(false); newsRecycler.setNestedScrollingEnabled(false);
// Sett tom adapter midlertidig // Sett tom adapter midlertidig
newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {}));
// "Se alle" knapp for nyheter // "Se alle" knapp for nyheter
TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); TextView viewAllNews = view.findViewById(R.id.btn_view_all_news);
if (viewAllNews != null) { if (viewAllNews != null) {
@ -118,26 +113,23 @@ public class HomeFragment extends Fragment {
// 1. Hent personlige hendelser først (fra CalendarManager) // 1. Hent personlige hendelser først (fra CalendarManager)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext()); List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// 2. Hent API-hendelser fra WordPress // 2. Hent API-hendelser DIREKTE fra Google
WordPressApiService apiService = RetrofitClient.getApiService(); String url = CalendarManager.getGoogleCalendarUrl();
apiService.getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override @Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return; if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>(); List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) { // Konverter fra Google-modell til KBS-modell
CalendarManager.formatEventForUI(e); // Formatér datoer apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents.add(e);
}
} }
// 3. Flett listene (API + Personlig) og sorter // 3. Flett listene (API + Personlig) og sorter
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
// 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) // 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag)
// (CalendarManager henter 1 år bakover, vi filtrere for "Topp 5 kommende")
List<CalendarEvent> upcomingEvents = new ArrayList<>(); List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
@ -160,14 +152,12 @@ public class HomeFragment extends Fragment {
} }
@Override @Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) { public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return; if (!isAdded()) return;
// Hvis API feiler, vis bare personlige events hvis vi har noen // Hvis API feiler, vis bare personlige events hvis vi har noen
if (!deviceEvents.isEmpty()) { if (!deviceEvents.isEmpty()) {
List<CalendarEvent> top5 = new ArrayList<>(); List<CalendarEvent> top5 = new ArrayList<>();
// Filtrer og plukk topp 5 fra lokale også
for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i)); for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i));
recyclerView.setAdapter(new CalendarAdapter(top5, event -> { recyclerView.setAdapter(new CalendarAdapter(top5, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.show(getParentFragmentManager(), "CalendarDetails"); sheet.show(getParentFragmentManager(), "CalendarDetails");
@ -201,7 +191,8 @@ public class HomeFragment extends Fragment {
for (WpPost post : wpPosts) { for (WpPost post : wpPosts) {
try { try {
Date date = rawFormat.parse(post.date); 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) {} } catch (Exception e) {}
} }

View file

@ -1,6 +1,13 @@
package com.kbs.kbsintranett; 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.os.Bundle;
import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@ -8,6 +15,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI; 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.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount; 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); bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment); .findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) { if (navHostFragment != null) {
navController = navHostFragment.getNavController(); navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNav, navController); 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 // 2. Start Silent Sign-In ved oppstart
checkLoginState(); checkLoginState();
} }
/**
* Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM).
* Hvis ikke, spør brukeren om å 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 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() { private void checkLoginState() {
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account == null) { if (account == null) {

View file

@ -1,12 +1,12 @@
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import android.app.NotificationChannel; import android.app.AlarmManager;
import android.app.NotificationManager; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
import java.io.IOException; import java.io.IOException;
@ -18,8 +18,7 @@ import retrofit2.Response;
public class NotificationWorker extends Worker { public class NotificationWorker extends Worker {
private static final String CHANNEL_ID = "kbs_calendar_channel"; private static final String TAG = "KBS_DEBUG";
private static final String PREFS_NAME = "KBSNotificationPrefs";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams); super(context, workerParams);
@ -28,72 +27,108 @@ public class NotificationWorker extends Worker {
@NonNull @NonNull
@Override @Override
public Result doWork() { public Result doWork() {
// Dette kjører i bakgrunnen Log.d(TAG, "NotificationWorker: Starter sjekk av kalender...");
try { try {
// Hent events synkront (ikke enqueue) String url = CalendarManager.getGoogleCalendarUrl();
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute(); // Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
checkAndNotify(response.body()); List<CalendarEvent> events = CalendarManager.convertGoogleResponse(response.body());
scheduleAlarms(events);
return Result.success(); return Result.success();
} else { } else {
Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code());
return Result.retry(); return Result.retry();
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "NotificationWorker: Nettverksfeil", e);
return Result.retry(); return Result.retry();
} }
} }
private void checkAndNotify(List<CalendarEvent> events) { private void scheduleAlarms(List<CalendarEvent> events) {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); Context context = getApplicationContext();
long now = System.currentTimeMillis(); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
long fifteenMinutes = 15 * 60 * 1000;
// 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 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) { for (CalendarEvent event : events) {
try { try {
Date eventDate; // Hopp over heldagshendelser (dato uten klokkeslett)
if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); if (event.getRawDate().length() == 10) continue;
else eventDate = sqlFormat.parse(event.getRawDate());
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 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; if (eventDate == null) continue;
long diff = eventDate.getTime() - now; // Beregn når alarmen skal
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
// Hvis eventet starter innen de neste 30 min, og ikke allerede varslet // Vi setter alarmen hvis tidspunktet er i fremtiden
if (diff > 0 && diff < (30 * 60 * 1000)) { // Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser)
String eventId = event.getTitle() + event.getRawDate(); // Enkel ID if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
boolean alreadyNotified = prefs.getBoolean(eventId, false);
if (!alreadyNotified) { // Lag en unik ID for alarmen
sendNotification(event.getTitle(), "Starter kl " + event.getTime()); String uniqueIdString = event.getTitle() + event.getRawDate();
// Lagre at vi har varslet int alarmId = uniqueIdString.hashCode();
prefs.edit().putBoolean(eventId, true).apply();
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 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) { } catch (Exception e) {
e.printStackTrace(); Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e);
}
} }
} }
private void sendNotification(String title, String content) { if (countSet == 0) {
NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);
} }
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());
} }
} }

View file

@ -15,6 +15,7 @@ import retrofit2.http.Multipart;
import retrofit2.http.Part; import retrofit2.http.Part;
import retrofit2.http.PartMap; import retrofit2.http.PartMap;
import retrofit2.http.Query; import retrofit2.http.Query;
import retrofit2.http.Url; // NY IMPORT
public interface WordPressApiService { public interface WordPressApiService {
@GET("wp-json/wp/v2/posts?per_page=10&_embed") @GET("wp-json/wp/v2/posts?per_page=10&_embed")
@ -40,9 +41,14 @@ public interface WordPressApiService {
@Part List<MultipartBody.Part> files @Part List<MultipartBody.Part> files
); );
// ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet
@GET("wp-json/kbs/v1/calendar/events") @GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents(); Call<List<CalendarEvent>> getCalendarEvents();
// NY: Direkte kall mot Google (bruker @Url for å override base URL)
@GET
Call<GoogleCalendarModels.Response> getDirectGoogleEvents(@Url String fullUrl);
@GET("wp-json/gf/v2/entries") @GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> getEntries( Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId, @Query("form_ids") int formId,

View file

@ -177,6 +177,7 @@ FILSTI: app\src\main\AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CALENDAR" /> <uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -202,6 +203,8 @@ FILSTI: app\src\main\AndroidManifest.xml
<activity android:name=".WebViewActivity" /> <activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider" android:authorities="com.kbs.kbsintranett.fileprovider"
@ -216,6 +219,75 @@ FILSTI: app\src\main\AndroidManifest.xml
</manifest> </manifest>
============================================================
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 FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
============================================================ ============================================================
@ -568,7 +640,6 @@ public class CalendarFullFragment extends Fragment {
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_full_calendar); recyclerView = view.findViewById(R.id.recycler_full_calendar);
progressBar = view.findViewById(R.id.loading_full_calendar); progressBar = view.findViewById(R.id.loading_full_calendar);
emptyView = view.findViewById(R.id.empty_view_calendar); emptyView = view.findViewById(R.id.empty_view_calendar);
@ -576,7 +647,6 @@ public class CalendarFullFragment extends Fragment {
layoutManager = new LinearLayoutManager(getContext()); layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp()); backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
fetchAllEvents(); fetchAllEvents();
@ -584,22 +654,21 @@ public class CalendarFullFragment extends Fragment {
private void fetchAllEvents() { private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
// Hent personlige hendelser (Nå med historikk) // Hent personlige hendelser (Nå med historikk)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext()); List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { // NYTT: Hent fra Google direkte
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override @Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return; if (!isAdded()) return;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>(); List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) { // Konverter og formatér
CalendarManager.formatEventForUI(e); apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents.add(e);
}
} }
// Flett og vis // Flett og vis
@ -624,7 +693,7 @@ public class CalendarFullFragment extends Fragment {
} }
@Override @Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) { public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return; if (!isAdded()) return;
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
@ -684,15 +753,86 @@ import java.util.TimeZone;
public class CalendarManager { 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<CalendarEvent> convertGoogleResponse(GoogleCalendarModels.Response response) {
List<CalendarEvent> 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 // NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) { private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();
String[] projection = new String[] { String[] projection = new String[] {
CalendarContract.Calendars._ID, CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME CalendarContract.Calendars.ACCOUNT_NAME
}; };
// Vi ser etter kontoer som slutter på @kbs.no // Vi ser etter kontoer som slutter på @kbs.no
// (SQL: account_name LIKE '%@kbs.no') // (SQL: account_name LIKE '%@kbs.no')
String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?"; String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?";
@ -707,7 +847,8 @@ public class CalendarManager {
)) { )) {
if (cursor != null) { if (cursor != null) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
ids.add(cursor.getString(0)); // Legg til kalender-ID ids.add(cursor.getString(0));
// Legg til kalender-ID
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -718,7 +859,6 @@ public class CalendarManager {
public static List<CalendarEvent> getDeviceEvents(Context context) { public static List<CalendarEvent> getDeviceEvents(Context context) {
List<CalendarEvent> deviceEvents = new ArrayList<>(); List<CalendarEvent> deviceEvents = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
return deviceEvents; return deviceEvents;
@ -726,7 +866,6 @@ public class CalendarManager {
// 1. Finn først ID-ene til KBS-kalenderne // 1. Finn først ID-ene til KBS-kalenderne
List<String> kbsCalendarIds = getKbsCalendarIds(context); List<String> kbsCalendarIds = getKbsCalendarIds(context);
// Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste // Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste
if (kbsCalendarIds.isEmpty()) { if (kbsCalendarIds.isEmpty()) {
return deviceEvents; return deviceEvents;
@ -736,7 +875,6 @@ public class CalendarManager {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long startMillis = now - (365L * 24 * 60 * 60 * 1000); long startMillis = now - (365L * 24 * 60 * 60 * 1000);
long endMillis = now + (365L * 24 * 60 * 60 * 1000); long endMillis = now + (365L * 24 * 60 * 60 * 1000);
String[] projection = new String[]{ String[] projection = new String[]{
CalendarContract.Events.TITLE, CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART, CalendarContract.Events.DTSTART,
@ -759,7 +897,6 @@ public class CalendarManager {
} }
} }
selection.append(") AND "); selection.append(") AND ");
selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND "); selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND ");
selection.append(CalendarContract.Events.DTSTART).append(" <= ?"); selection.append(CalendarContract.Events.DTSTART).append(" <= ?");
@ -767,7 +904,6 @@ public class CalendarManager {
selectionArgsList.add(String.valueOf(endMillis)); selectionArgsList.add(String.valueOf(endMillis));
String[] selectionArgs = selectionArgsList.toArray(new String[0]); String[] selectionArgs = selectionArgsList.toArray(new String[0]);
try (Cursor cursor = context.getContentResolver().query( try (Cursor cursor = context.getContentResolver().query(
CalendarContract.Events.CONTENT_URI, CalendarContract.Events.CONTENT_URI,
projection, projection,
@ -777,7 +913,6 @@ public class CalendarManager {
)) { )) {
if (cursor != null) { if (cursor != null) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()); SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
String title = cursor.getString(0); String title = cursor.getString(0);
long dtStart = cursor.getLong(1); long dtStart = cursor.getLong(1);
@ -785,7 +920,6 @@ public class CalendarManager {
String desc = cursor.getString(3); String desc = cursor.getString(3);
String loc = cursor.getString(4); String loc = cursor.getString(4);
int allDay = cursor.getInt(5); int allDay = cursor.getInt(5);
String rawStart; String rawStart;
String rawEnd; String rawEnd;
@ -809,14 +943,11 @@ public class CalendarManager {
return deviceEvents; return deviceEvents;
} }
// --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) ---
public static void formatEventForUI(CalendarEvent event) { public static void formatEventForUI(CalendarEvent event) {
if (event.getRawDate() == null) return; if (event.getRawDate() == null) return;
SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault()); SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault());
SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO")); SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO"));
SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault()); SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
@ -826,7 +957,6 @@ public class CalendarManager {
boolean isAllDay = false; boolean isAllDay = false;
String raw = event.getRawDate(); String raw = event.getRawDate();
if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) { if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) {
SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
date = shortFmt.parse(raw); date = shortFmt.parse(raw);
@ -884,7 +1014,6 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : ""; String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2); return d1.compareTo(d2);
}); });
return all; 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<Item> 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<Override> 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 FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java
============================================================ ============================================================
@ -3767,7 +3959,6 @@ public class HomeFragment extends Fragment {
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Håndter svar på kalendertillatelse // Håndter svar på kalendertillatelse
requestPermissionLauncher = registerForActivityResult( requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(), new ActivityResultContracts.RequestPermission(),
@ -3778,7 +3969,6 @@ public class HomeFragment extends Fragment {
} }
} }
); );
// Start bakgrunnsjobb for varsling (kjører hver 15. minutt) // Start bakgrunnsjobb for varsling (kjører hver 15. minutt)
startNotificationWorker(); startNotificationWorker();
} }
@ -3792,7 +3982,6 @@ public class HomeFragment extends Fragment {
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// 0. Profil-knapp // 0. Profil-knapp
View profileBtn = view.findViewById(R.id.btn_profile); View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) { if (profileBtn != null) {
@ -3804,7 +3993,6 @@ public class HomeFragment extends Fragment {
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext())); calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
// Sett tom adapter midlertidig // Sett tom adapter midlertidig
calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {})); calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
// "Se alle" knapp for kalender // "Se alle" knapp for kalender
TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar); TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar);
if (viewAllCalendar != null) { if (viewAllCalendar != null) {
@ -3832,7 +4020,6 @@ public class HomeFragment extends Fragment {
newsRecycler.setNestedScrollingEnabled(false); newsRecycler.setNestedScrollingEnabled(false);
// Sett tom adapter midlertidig // Sett tom adapter midlertidig
newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {})); newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {}));
// "Se alle" knapp for nyheter // "Se alle" knapp for nyheter
TextView viewAllNews = view.findViewById(R.id.btn_view_all_news); TextView viewAllNews = view.findViewById(R.id.btn_view_all_news);
if (viewAllNews != null) { if (viewAllNews != null) {
@ -3848,26 +4035,23 @@ public class HomeFragment extends Fragment {
// 1. Hent personlige hendelser først (fra CalendarManager) // 1. Hent personlige hendelser først (fra CalendarManager)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext()); List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// 2. Hent API-hendelser fra WordPress // 2. Hent API-hendelser DIREKTE fra Google
WordPressApiService apiService = RetrofitClient.getApiService(); String url = CalendarManager.getGoogleCalendarUrl();
apiService.getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() { RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override @Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) { public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return; if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>(); List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) { // Konverter fra Google-modell til KBS-modell
CalendarManager.formatEventForUI(e); // Formatér datoer apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents.add(e);
}
} }
// 3. Flett listene (API + Personlig) og sorter // 3. Flett listene (API + Personlig) og sorter
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents); List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
// 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag) // 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<CalendarEvent> upcomingEvents = new ArrayList<>(); List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date()); String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
@ -3890,14 +4074,12 @@ public class HomeFragment extends Fragment {
} }
@Override @Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) { public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return; if (!isAdded()) return;
// Hvis API feiler, vis bare personlige events hvis vi har noen // Hvis API feiler, vis bare personlige events hvis vi har noen
if (!deviceEvents.isEmpty()) { if (!deviceEvents.isEmpty()) {
List<CalendarEvent> top5 = new ArrayList<>(); List<CalendarEvent> top5 = new ArrayList<>();
// Filtrer og plukk topp 5 fra lokale også
for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i)); for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i));
recyclerView.setAdapter(new CalendarAdapter(top5, event -> { recyclerView.setAdapter(new CalendarAdapter(top5, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event); CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.show(getParentFragmentManager(), "CalendarDetails"); sheet.show(getParentFragmentManager(), "CalendarDetails");
@ -3931,7 +4113,8 @@ public class HomeFragment extends Fragment {
for (WpPost post : wpPosts) { for (WpPost post : wpPosts) {
try { try {
Date date = rawFormat.parse(post.date); 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) {} } catch (Exception e) {}
} }
@ -4314,7 +4497,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java
============================================================ ============================================================
package com.kbs.kbsintranett; 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.os.Bundle;
import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@ -4322,6 +4512,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI; 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.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount; 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); bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment); .findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) { if (navHostFragment != null) {
navController = navHostFragment.getNavController(); navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNav, navController); 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 // 2. Start Silent Sign-In ved oppstart
checkLoginState(); 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() { private void checkLoginState() {
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this); GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account == null) { if (account == null) {
@ -4780,13 +5013,13 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationWorker.java
============================================================ ============================================================
package com.kbs.kbsintranett; package com.kbs.kbsintranett;
import android.app.NotificationChannel; import android.app.AlarmManager;
import android.app.NotificationManager; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
import java.io.IOException; import java.io.IOException;
@ -4798,8 +5031,7 @@ import retrofit2.Response;
public class NotificationWorker extends Worker { public class NotificationWorker extends Worker {
private static final String CHANNEL_ID = "kbs_calendar_channel"; private static final String TAG = "KBS_DEBUG";
private static final String PREFS_NAME = "KBSNotificationPrefs";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams); super(context, workerParams);
@ -4808,73 +5040,109 @@ public class NotificationWorker extends Worker {
@NonNull @NonNull
@Override @Override
public Result doWork() { public Result doWork() {
// Dette kjører i bakgrunnen Log.d(TAG, "NotificationWorker: Starter sjekk av kalender...");
try { try {
// Hent events synkront (ikke enqueue) String url = CalendarManager.getGoogleCalendarUrl();
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute(); // Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) { if (response.isSuccessful() && response.body() != null) {
checkAndNotify(response.body()); List<CalendarEvent> events = CalendarManager.convertGoogleResponse(response.body());
scheduleAlarms(events);
return Result.success(); return Result.success();
} else { } else {
Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code());
return Result.retry(); return Result.retry();
} }
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "NotificationWorker: Nettverksfeil", e);
return Result.retry(); return Result.retry();
} }
} }
private void checkAndNotify(List<CalendarEvent> events) { private void scheduleAlarms(List<CalendarEvent> events) {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); Context context = getApplicationContext();
long now = System.currentTimeMillis(); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
long fifteenMinutes = 15 * 60 * 1000;
// 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 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) { for (CalendarEvent event : events) {
try { try {
Date eventDate; // Hopp over heldagshendelser (dato uten klokkeslett)
if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate()); if (event.getRawDate().length() == 10) continue;
else eventDate = sqlFormat.parse(event.getRawDate());
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; 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 // Vi setter alarmen hvis tidspunktet er i fremtiden
if (diff > 0 && diff < (30 * 60 * 1000)) { // Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser)
String eventId = event.getTitle() + event.getRawDate(); // Enkel ID if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
boolean alreadyNotified = prefs.getBoolean(eventId, false);
if (!alreadyNotified) { // Lag en unik ID for alarmen
sendNotification(event.getTitle(), "Starter kl " + event.getTime()); String uniqueIdString = event.getTitle() + event.getRawDate();
// Lagre at vi har varslet int alarmId = uniqueIdString.hashCode();
prefs.edit().putBoolean(eventId, true).apply();
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) { } catch (Exception e) {
e.printStackTrace(); Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e);
}
} }
} }
private void sendNotification(String title, String content) { if (countSet == 0) {
NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);
} }
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.Part;
import retrofit2.http.PartMap; import retrofit2.http.PartMap;
import retrofit2.http.Query; import retrofit2.http.Query;
import retrofit2.http.Url; // NY IMPORT
public interface WordPressApiService { public interface WordPressApiService {
@GET("wp-json/wp/v2/posts?per_page=10&_embed") @GET("wp-json/wp/v2/posts?per_page=10&_embed")
@ -5272,9 +5541,14 @@ public interface WordPressApiService {
@Part List<MultipartBody.Part> files @Part List<MultipartBody.Part> files
); );
// ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet
@GET("wp-json/kbs/v1/calendar/events") @GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents(); Call<List<CalendarEvent>> getCalendarEvents();
// NY: Direkte kall mot Google (bruker @Url for å override base URL)
@GET
Call<GoogleCalendarModels.Response> getDirectGoogleEvents(@Url String fullUrl);
@GET("wp-json/gf/v2/entries") @GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> getEntries( Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId, @Query("form_ids") int formId,