Før vi snur kalenderprosjektet

This commit is contained in:
ErolHaagenrud 2025-12-16 16:23:23 +01:00
parent 6f3fd9059d
commit 544cda4ce4
6 changed files with 98 additions and 174 deletions

View file

@ -27,7 +27,7 @@ public class CalendarFullFragment extends Fragment {
private RecyclerView recyclerView;
private ProgressBar progressBar;
private TextView emptyView;
private LinearLayoutManager layoutManager; // Trenger denne for å scrolle
private LinearLayoutManager layoutManager;
@Nullable
@Override
@ -52,24 +52,25 @@ public class CalendarFullFragment extends Fragment {
private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE);
// Hent personlige hendelser ( med historikk)
// Hent personlige hendelser
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// NYTT: Hent fra Google direkte
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
// Hent felles hendelser fra WordPress Proxy (sikrer at vi får reminders)
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
// Konverter og formatér
apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents = response.body();
// Formater data for visning
for (CalendarEvent e : apiEvents) {
CalendarManager.formatEventForUI(e);
}
}
// Flett og vis
List<CalendarEvent> allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
if (allEvents.isEmpty()) {
@ -85,13 +86,12 @@ public class CalendarFullFragment extends Fragment {
});
recyclerView.setAdapter(adapter);
// --- SCROLL TIL I DAG ---
scrollToToday(allEvents);
}
}
@Override
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
@ -114,7 +114,6 @@ public class CalendarFullFragment extends Fragment {
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
int scrollIndex = 0;
// Finn første event som er i dag eller senere
for (int i = 0; i < events.size(); i++) {
String raw = events.get(i).getRawDate();
if (raw != null && raw.compareTo(today) >= 0) {
@ -123,10 +122,9 @@ public class CalendarFullFragment extends Fragment {
}
}
// Scroll litt ned slik at "i dag" havner toppen, men ikke helt (offset 0)
// Bruker scrollToPositionWithOffset for presisjon
if (scrollIndex > 0) {
layoutManager.scrollToPositionWithOffset(scrollIndex, 0);
}
}
}

View file

@ -4,7 +4,6 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.CalendarContract;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -15,96 +14,6 @@ import java.util.Locale;
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() {
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;
}
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;
}
if (item.end != null) {
if (item.end.dateTime != null) end = item.end.dateTime;
else end = item.end.date;
}
// --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) ---
if (title.toLowerCase().contains("test")) {
Log.d("KBS_DEBUG", "--------------------------------------------------");
Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title);
Log.d("KBS_DEBUG", " Starttid: " + start);
if (item.reminders != null) {
Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault);
if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:");
for (GoogleCalendarModels.Override override : item.reminders.overrides) {
Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes);
}
} else {
Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google.");
}
} else {
Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen.");
}
Log.d("KBS_DEBUG", "--------------------------------------------------");
}
// -------------------------------------------------
// Varslings-logikk
int reminderMinutes = 15; // Default fallback
if (item.reminders != null) {
if (item.reminders.useDefault) {
reminderMinutes = 15;
} else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
// Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter
reminderMinutes = item.reminders.overrides.get(0).minutes;
}
}
CalendarEvent event = new CalendarEvent(title, start, end, desc, loc);
event.setReminderMinutes(reminderMinutes);
formatEventForUI(event);
events.add(event);
}
return events;
}
// --- RESTERENDE KODE (UENDRET) ---
private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
@ -209,6 +118,12 @@ public class CalendarManager {
}
CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc);
// For lokale events bruker vi ikke reminderMinutes fra API,
// men systemet håndterer varsling selv for lokale kalendere.
// Vi setter den til 0 her for å unngå doble varsler fra appen.
event.setReminderMinutes(0);
formatEventForUI(event);
deviceEvents.add(event);
}
@ -296,4 +211,5 @@ public class CalendarManager {
return all;
}
}

View file

@ -0,0 +1,4 @@
package com.kbs.kbsintranett;
public class CreateEventRequest {
}

View file

@ -37,17 +37,14 @@ public class HomeFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Håndter svar kalendertillatelse
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
isGranted -> {
// Prøv å laste kalender nytt ( potensielt med personlig kalender)
if (calendarRecycler != null) {
fetchCalendarEvents(calendarRecycler);
}
}
);
// Start bakgrunnsjobb for varsling (kjører hver 15. minutt)
startNotificationWorker();
}
@ -60,45 +57,38 @@ 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) {
profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile));
}
// 1. Kalender oppsett
calendarRecycler = view.findViewById(R.id.recycler_calendar);
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) {
viewAllCalendar.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.action_home_to_calendarFull));
}
// Sjekk tillatelse for personlig kalender
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) {
fetchCalendarEvents(calendarRecycler);
} else {
// Spør om lov til å lese kalender
requestPermissionLauncher.launch(Manifest.permission.READ_CALENDAR);
}
// Spør også om lov til å sende varsler (Android 13+)
if (android.os.Build.VERSION.SDK_INT >= 33) {
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}).launch(Manifest.permission.POST_NOTIFICATIONS);
}
}
// 2. Nyheter oppsett
RecyclerView newsRecycler = view.findViewById(R.id.recycler_news);
newsRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
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) {
viewAllNews.setOnClickListener(v -> {
@ -110,26 +100,28 @@ public class HomeFragment extends Fragment {
}
private void fetchCalendarEvents(RecyclerView recyclerView) {
// 1. Hent personlige hendelser først (fra CalendarManager)
// 1. Hent personlige hendelser
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// 2. Hent API-hendelser DIREKTE fra Google
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
// 2. Hent felles hendelser fra WordPress (Proxy mot Google)
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
// Konverter fra Google-modell til KBS-modell
apiEvents = CalendarManager.convertGoogleResponse(response.body());
apiEvents = response.body();
// Formater datoer for visning (UI-logikk)
for (CalendarEvent e : apiEvents) {
CalendarManager.formatEventForUI(e);
}
}
// 3. Flett listene (API + Personlig) og sorter
// 3. Flett og sorter
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
// 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag)
// 4. Filtrer (kun fremtidige + i dag)
List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
@ -139,7 +131,7 @@ public class HomeFragment extends Fragment {
}
}
// 5. Vis kun de 5 første av de kommende
// 5. Vis topp 5
List<CalendarEvent> top5 = new ArrayList<>();
for(int i=0; i<Math.min(upcomingEvents.size(), 5); i++) {
top5.add(upcomingEvents.get(i));
@ -152,9 +144,9 @@ public class HomeFragment extends Fragment {
}
@Override
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
if (!isAdded()) return;
// Hvis API feiler, vis bare personlige events hvis vi har noen
// Fallback til kun lokale events
if (!deviceEvents.isEmpty()) {
List<CalendarEvent> top5 = new ArrayList<>();
for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i));
@ -173,7 +165,6 @@ public class HomeFragment extends Fragment {
private void fetchNewsFromWordpress(RecyclerView recyclerView) {
WordPressApiService apiService = RetrofitClient.getApiService();
// Bruker getPosts som henter 5-10 innlegg med ?_embed
apiService.getPosts().enqueue(new Callback<List<WpPost>>() {
@Override
public void onResponse(Call<List<WpPost>> call, Response<List<WpPost>> response) {
@ -181,8 +172,6 @@ public class HomeFragment extends Fragment {
if (response.isSuccessful() && response.body() != null) {
List<WpPost> wpPosts = response.body();
// Datoformatering
SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
@ -192,15 +181,12 @@ public class HomeFragment extends Fragment {
try {
Date date = rawFormat.parse(post.date);
post.date = targetFormat.format(date);
// Setter pen dato
} catch (Exception e) {}
}
// Sett adapter med Click Listener
NewsAdapter adapter = new NewsAdapter(wpPosts, post -> {
// Naviger til detaljvisning og send med post-objektet
Bundle bundle = new Bundle();
bundle.putSerializable("post_data", post); // WpPost er Serializable
bundle.putSerializable("post_data", post);
Navigation.findNavController(getView()).navigate(R.id.action_home_to_newsDetail, bundle);
});
recyclerView.setAdapter(adapter);
@ -210,17 +196,16 @@ public class HomeFragment extends Fragment {
@Override
public void onFailure(Call<List<WpPost>> call, Throwable t) {
if (getContext() == null) return;
// Ved feil, sett tom liste (eller vis feilmelding)
recyclerView.setAdapter(new NewsAdapter(new ArrayList<>(), null));
}
});
}
private void startNotificationWorker() {
// Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter i HMS-kalenderen
PeriodicWorkRequest notifRequest =
new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(requireContext()).enqueue(notifRequest);
}
}

View file

@ -17,7 +17,6 @@ import java.util.Locale;
import retrofit2.Response;
public class NotificationWorker extends Worker {
private static final String TAG = "KBS_DEBUG";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
@ -27,18 +26,22 @@ public class NotificationWorker extends Worker {
@NonNull
@Override
public Result doWork() {
Log.d(TAG, "NotificationWorker: Starter sjekk av kalender...");
Log.d(TAG, "NotificationWorker: Starter sjekk av kalender via WordPress Proxy...");
try {
String url = CalendarManager.getGoogleCalendarUrl();
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
if (response.isSuccessful() && response.body() != null) {
List<CalendarEvent> events = CalendarManager.convertGoogleResponse(response.body());
List<CalendarEvent> events = response.body();
scheduleAlarms(events);
return Result.success();
} else {
Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code());
int code = response.code();
Log.e(TAG, "NotificationWorker: API-kall mot WP feilet. Kode: " + code);
// Stopp retry ved klientfeil (4xx) inkludert 429
if (code >= 400 && code < 500) {
return Result.failure();
}
return Result.retry();
}
} catch (IOException e) {
@ -53,54 +56,73 @@ public class NotificationWorker extends Worker {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!");
Log.e(TAG, "NotificationWorker: MANGLER tillatelse for nøyaktige alarmer!");
return;
}
}
long now = System.currentTimeMillis();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
// Tillater alarmer som skulle gått for inntil 30 minutter siden (Catch-up)
long catchUpWindow = now - (30 * 60 * 1000L);
// Setter ikke alarmer lenger frem enn 24 timer
long futureWindow = now + (24 * 60 * 60 * 1000L);
int countSet = 0;
for (CalendarEvent event : events) {
try {
if (event.getRawDate().length() == 10) continue;
String title = event.getTitle();
// Ignorer heldagshendelser (datoformat uten klokkeslett)
if (event.getRawDate() == null || event.getRawDate().length() == 10) continue;
Date eventDate = null;
if (event.getRawDate().contains("T")) {
String raw = event.getRawDate();
// Kutter tidssone for å parse som lokal tid "face value"
if (raw.length() > 19) raw = raw.substring(0, 19);
eventDate = isoFormat.parse(raw);
}
if (eventDate == null) continue;
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
int minutesBefore = event.getReminderMinutes();
long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L);
// --- DETALJERT LOGGING FOR Å FEILSØKE ---
if (event.getTitle().toLowerCase().contains("test")) {
Log.d(TAG, "SJEKKER TEST-EVENT:");
Log.d(TAG, " Start: " + eventDate);
Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før");
Log.d(TAG, " Alarmtid: " + new Date(triggerTime));
Log.d(TAG, " Nå: " + new Date(now));
// --- DIAGNOSE: Logg alle "Test"-events for å se hva PHP faktisk leverer ---
if (title.toLowerCase().contains("test")) {
Log.d(TAG, "--------------------------------------------------");
Log.d(TAG, "DIAGNOSE EVENT: " + title);
Log.d(TAG, " Starttid: " + eventDate);
Log.d(TAG, " PHP sier reminderMinutes: " + minutesBefore);
if (triggerTime > now) {
Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)");
if (minutesBefore <= 0) {
Log.d(TAG, " HANDLING: Skippes (Ingen varsling)");
} else {
Log.d(TAG, " Status: I FORTIDEN (Hoppes over)");
Log.d(TAG, " Ønsket alarmtid: " + new Date(triggerTime));
Log.d(TAG, " Nå: " + new Date(now));
}
}
// ----------------------------------------
// ------------------------------------------------------------------------
if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
if (minutesBefore <= 0) continue;
String uniqueIdString = event.getTitle() + event.getRawDate();
int alarmId = uniqueIdString.hashCode();
if (triggerTime > catchUpWindow && triggerTime < futureWindow) {
// CATCH-UP: Hvis tiden har passert, fyr av .
if (triggerTime < now) {
Log.i(TAG, " -> Alarmtid passert (" + new Date(triggerTime) + "). Kjører Catch-up NÅ.");
triggerTime = now + 1000;
}
int alarmId = (title + event.getRawDate()).hashCode();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("TITLE", event.getTitle());
intent.putExtra("MESSAGE", "Starter kl " + event.getTime());
intent.putExtra("TITLE", title);
String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate);
intent.putExtra("MESSAGE", "Starter kl " + timeStr);
intent.putExtra("ID", alarmId);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
@ -116,7 +138,10 @@ public class NotificationWorker extends Worker {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
}
Log.i(TAG, ">>> ALARM SATT: " + event.getTitle());
if (title.toLowerCase().contains("test")) {
Log.d(TAG, " RESULTAT: ALARM SATT OK");
Log.d(TAG, "--------------------------------------------------");
}
countSet++;
}
@ -126,7 +151,7 @@ public class NotificationWorker extends Worker {
}
if (countSet == 0) {
Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t).");
Log.d(TAG, "Ingen nye alarmer satt i denne runden.");
}
}
}

View file

@ -1,7 +1,7 @@
package com.kbs.kbsintranett;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; // NY IMPORT
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import okhttp3.MultipartBody;
@ -15,7 +15,6 @@ 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")
@ -41,14 +40,11 @@ public interface WordPressApiService {
@Part List<MultipartBody.Part> files
);
// ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet
// BRUKES : Henter kalenderhendelser via WordPress proxy.
// Dette sikrer at vi får med reminder_minutes fra serveren.
@GET("wp-json/kbs/v1/calendar/events")
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")
Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId,
@ -68,7 +64,7 @@ public interface WordPressApiService {
@GET("wp-json/kbs/v1/handbook/{id}")
Call<HandbookPage> getHandbookPage(@Path("id") int id);
// NY: Slå opp ID fra URL
@GET("wp-json/kbs/v1/lookup-id")
Call<JsonObject> lookupPageId(@Query("url") String url);
}