diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 85220b6..6695f08 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -54,4 +54,7 @@ dependencies {
implementation("com.google.android.gms:play-services-auth:20.7.0")
implementation("com.github.bumptech.glide:glide:4.16.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ implementation("androidx.work:work-runtime:2.9.0")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dfef0fc..3d47831 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,6 +18,12 @@
+
+
+
+
+
+
{ // [cite: 31]
+public class CalendarAdapter extends RecyclerView.Adapter {
private List events;
- public CalendarAdapter(List events) { // [cite: 32]
+ private final OnItemClickListener listener;
+
+ public interface OnItemClickListener {
+ void onItemClick(CalendarEvent event);
+ }
+
+ // Oppdatert konstruktør som tar imot en listener
+ public CalendarAdapter(List events, OnItemClickListener listener) {
this.events = events;
- } // [cite: 33]
+ this.listener = listener;
+ }
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar, parent, false);
- return new ViewHolder(view); // [cite: 34]
+ return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CalendarEvent event = events.get(position);
- holder.day.setText(event.getDay()); // [cite: 35]
+ holder.day.setText(event.getDay());
holder.month.setText(event.getMonth());
- // NYTT: Tidspunktet hentes nå fra getTime() som formateres i HomeFragment.
holder.time.setText(event.getTime());
holder.title.setText(event.getTitle());
+
+ holder.itemView.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onItemClick(event);
+ }
+ });
}
@Override
public int getItemCount() {
- return events.size(); // [cite: 36]
+ return events.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
- TextView day, month, title, time; // NYTT: Lagt til time
- public ViewHolder(View view) { // [cite: 37]
+ TextView day, month, title, time;
+
+ public ViewHolder(View view) {
super(view);
day = view.findViewById(R.id.cal_day);
- month = view.findViewById(R.id.cal_month); // [cite: 38]
+ month = view.findViewById(R.id.cal_month);
title = view.findViewById(R.id.cal_title);
- time = view.findViewById(R.id.cal_time); // NYTT
+ time = view.findViewById(R.id.cal_time);
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java b/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java
new file mode 100644
index 0000000..2d3db3b
--- /dev/null
+++ b/app/src/main/java/com/kbs/kbsintranett/CalendarDetailsBottomSheet.java
@@ -0,0 +1,93 @@
+package com.kbs.kbsintranett;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.CalendarContract;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class CalendarDetailsBottomSheet extends BottomSheetDialogFragment {
+
+ private CalendarEvent event;
+
+ public CalendarDetailsBottomSheet(CalendarEvent event) {
+ this.event = event;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.bottom_sheet_calendar_details, container, false);
+
+ TextView title = view.findViewById(R.id.sheet_title);
+ TextView time = view.findViewById(R.id.sheet_time);
+ TextView desc = view.findViewById(R.id.sheet_desc);
+ TextView loc = view.findViewById(R.id.sheet_location);
+ Button btnAdd = view.findViewById(R.id.btn_add_to_calendar);
+
+ // Skjul knapp siden appen nå varsler automatisk (iht krav)
+ btnAdd.setVisibility(View.GONE);
+
+ title.setText(event.getTitle());
+ time.setText(event.getTime() + " (" + event.getDay() + ". " + event.getMonth() + ")");
+
+ if (!event.getDescription().isEmpty()) {
+ // HER ER FIKSEN FOR HTML:
+ desc.setText(android.text.Html.fromHtml(event.getDescription(), android.text.Html.FROM_HTML_MODE_COMPACT));
+ desc.setVisibility(View.VISIBLE);
+ // Gjør linker klikkbare
+ desc.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
+ } else {
+ desc.setVisibility(View.GONE);
+ }
+
+ if (!event.getLocation().isEmpty()) {
+ loc.setText("Sted: " + event.getLocation());
+ loc.setVisibility(View.VISIBLE);
+ } else {
+ loc.setVisibility(View.GONE);
+ }
+
+ return view;
+ }
+
+ private void addToSystemCalendar() {
+ try {
+ SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
+
+ Date startDate = apiFormat.parse(event.getRawDate());
+ long startMillis = startDate.getTime();
+ long endMillis = startMillis + (60 * 60 * 1000); // Default 1 time hvis slutt mangler
+
+ if (event.getRawEndDate() != null && !event.getRawEndDate().isEmpty()) {
+ Date endDate = apiFormat.parse(event.getRawEndDate());
+ endMillis = endDate.getTime();
+ }
+
+ Intent intent = new Intent(Intent.ACTION_INSERT)
+ .setData(CalendarContract.Events.CONTENT_URI)
+ .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis)
+ .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis)
+ .putExtra(CalendarContract.Events.TITLE, event.getTitle())
+ .putExtra(CalendarContract.Events.DESCRIPTION, event.getDescription())
+ .putExtra(CalendarContract.Events.EVENT_LOCATION, event.getLocation())
+ .putExtra(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY);
+
+ startActivity(intent);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java
index 983c4e3..bb331ea 100644
--- a/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java
+++ b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java
@@ -1,13 +1,38 @@
-// FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarEvent.java
package com.kbs.kbsintranett;
-public class CalendarEvent {
- private String title;
- private String rawDate; // NYTT: Holder den fulle, u-formaterte dato/tid-strengen fra API'et
- private String day; // F.eks "12"
- private String month; // F.eks "DES"
- private String time; // NYTT: Brukes kun for visning av tid
+import com.google.gson.annotations.SerializedName;
+public class CalendarEvent {
+ @SerializedName("title")
+ private String title;
+
+ @SerializedName("start_date") // Juster denne nøkkelen til hva APIet faktisk returnerer (f.eks "start")
+ private String rawDate;
+
+ @SerializedName("end_date") // Juster nøkkel (f.eks "end")
+ private String rawEndDate;
+
+ @SerializedName("description")
+ private String description;
+
+ @SerializedName("location")
+ private String location;
+
+ // --- UI-hjelpefelter (settes manuelt i appen etter parsing) ---
+ private String day; // F.eks "12"
+ private String month; // F.eks "DES"
+ private String time; // F.eks "10:00 - 11:30"
+
+ // Konstruktør for Retrofit (Gson)
+ public CalendarEvent(String title, String rawDate, String rawEndDate, String description, String location) {
+ this.title = title;
+ this.rawDate = rawDate;
+ this.rawEndDate = rawEndDate;
+ this.description = description;
+ this.location = location;
+ }
+
+ // Konstruktør for manuell opprettelse (f.eks ved feil)
public CalendarEvent(String title, String time, String day, String month) {
this.title = title;
this.time = time;
@@ -15,16 +40,19 @@ public class CalendarEvent {
this.month = month;
}
- public CalendarEvent(String title, String rawDate) {
- this.title = title;
- this.rawDate = rawDate;
- // La de andre feltene være null i starten, de fylles i HomeFragment
- }
-
public String getTitle() { return title; }
- public String getTime() { return time; }
- public String getDay() { return day; }
- public String getMonth() { return month; }
+ public String getRawDate() { return rawDate; }
+ public String getRawEndDate() { return rawEndDate; }
+ public String getDescription() { return description != null ? description : ""; }
+ public String getLocation() { return location != null ? location : ""; }
- public String getRawDate() { return rawDate; } // NYTT
+ // Getters og Setters for UI-felter
+ public String getDay() { return day; }
+ public void setDay(String day) { this.day = day; }
+
+ public String getMonth() { return month; }
+ public void setMonth(String month) { this.month = month; }
+
+ public String getTime() { return time; }
+ public void setTime(String time) { this.time = time; }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java b/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java
new file mode 100644
index 0000000..601a4a5
--- /dev/null
+++ b/app/src/main/java/com/kbs/kbsintranett/CalendarFullFragment.java
@@ -0,0 +1,135 @@
+package com.kbs.kbsintranett;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.Navigation;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class CalendarFullFragment extends Fragment {
+
+ private RecyclerView recyclerView;
+ private ProgressBar progressBar;
+ private TextView emptyView;
+ private LinearLayoutManager layoutManager; // Trenger denne for å scrolle
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_calendar_full, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ recyclerView = view.findViewById(R.id.recycler_full_calendar);
+ progressBar = view.findViewById(R.id.loading_full_calendar);
+ emptyView = view.findViewById(R.id.empty_view_calendar);
+ ImageView backBtn = view.findViewById(R.id.btn_back_calendar);
+
+ layoutManager = new LinearLayoutManager(getContext());
+ recyclerView.setLayoutManager(layoutManager);
+
+ backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
+
+ fetchAllEvents();
+ }
+
+ private void fetchAllEvents() {
+ progressBar.setVisibility(View.VISIBLE);
+
+ // Hent personlige hendelser (Nå med historikk)
+ List deviceEvents = CalendarManager.getDeviceEvents(getContext());
+
+ RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (!isAdded()) return;
+ progressBar.setVisibility(View.GONE);
+
+ List apiEvents = new ArrayList<>();
+ if (response.isSuccessful() && response.body() != null) {
+ for (CalendarEvent e : response.body()) {
+ CalendarManager.formatEventForUI(e);
+ apiEvents.add(e);
+ }
+ }
+
+ // Flett og vis
+ List allEvents = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
+
+ if (allEvents.isEmpty()) {
+ emptyView.setVisibility(View.VISIBLE);
+ recyclerView.setVisibility(View.GONE);
+ } else {
+ emptyView.setVisibility(View.GONE);
+ recyclerView.setVisibility(View.VISIBLE);
+
+ CalendarAdapter adapter = new CalendarAdapter(allEvents, event -> {
+ CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
+ sheet.show(getParentFragmentManager(), "CalendarDetails");
+ });
+ recyclerView.setAdapter(adapter);
+
+ // --- SCROLL TIL I DAG ---
+ scrollToToday(allEvents);
+ }
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t) {
+ if (!isAdded()) return;
+ progressBar.setVisibility(View.GONE);
+
+ if (!deviceEvents.isEmpty()) {
+ CalendarAdapter adapter = new CalendarAdapter(deviceEvents, event -> {
+ CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
+ sheet.show(getParentFragmentManager(), "CalendarDetails");
+ });
+ recyclerView.setAdapter(adapter);
+ scrollToToday(deviceEvents);
+ } else {
+ emptyView.setText("Ingen hendelser funnet.");
+ emptyView.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+ }
+
+ private void scrollToToday(List events) {
+ 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) {
+ scrollIndex = i;
+ break;
+ }
+ }
+
+ // Scroll litt ned slik at "i dag" havner på toppen, men ikke helt (offset 0)
+ // Bruker scrollToPositionWithOffset for presisjon
+ if (scrollIndex > 0) {
+ layoutManager.scrollToPositionWithOffset(scrollIndex, 0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java
new file mode 100644
index 0000000..fb6af0c
--- /dev/null
+++ b/app/src/main/java/com/kbs/kbsintranett/CalendarManager.java
@@ -0,0 +1,174 @@
+package com.kbs.kbsintranett;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.provider.CalendarContract;
+import androidx.core.content.ContextCompat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class CalendarManager {
+
+ public static List getDeviceEvents(Context context) {
+ List deviceEvents = new ArrayList<>();
+
+ if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
+ != PackageManager.PERMISSION_GRANTED) {
+ return deviceEvents;
+ }
+
+ // ENDRET: Hent events fra 1 år tilbake og 1 år frem
+ long now = System.currentTimeMillis();
+ long startMillis = now - (365L * 24 * 60 * 60 * 1000); // 1 år tilbake
+ long endMillis = now + (365L * 24 * 60 * 60 * 1000); // 1 år frem
+
+ String[] projection = new String[]{
+ CalendarContract.Events.TITLE,
+ CalendarContract.Events.DTSTART,
+ CalendarContract.Events.DTEND,
+ CalendarContract.Events.DESCRIPTION,
+ CalendarContract.Events.EVENT_LOCATION,
+ CalendarContract.Events.ALL_DAY // Nyttig for å vite om det er heldags
+ };
+
+ String selection = CalendarContract.Events.DTSTART + " >= ? AND " + CalendarContract.Events.DTSTART + " <= ?";
+ String[] selectionArgs = new String[]{String.valueOf(startMillis), String.valueOf(endMillis)};
+
+ try (Cursor cursor = context.getContentResolver().query(
+ CalendarContract.Events.CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ CalendarContract.Events.DTSTART + " ASC"
+ )) {
+ if (cursor != null) {
+ // Vi bruker ISO format internt for sortering
+ SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
+
+ while (cursor.moveToNext()) {
+ String title = cursor.getString(0);
+ long dtStart = cursor.getLong(1);
+ long dtEnd = cursor.getLong(2);
+ String desc = cursor.getString(3);
+ String loc = cursor.getString(4);
+ int allDay = cursor.getInt(5);
+
+ String rawStart;
+ String rawEnd;
+
+ if (allDay == 1) {
+ // For heldags lagrer vi bare datoen: yyyy-MM-dd
+ SimpleDateFormat shortFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ rawStart = shortFormat.format(new Date(dtStart));
+ rawEnd = shortFormat.format(new Date(dtEnd));
+ } else {
+ // For vanlige events bruker vi full tid
+ rawStart = isoFormat.format(new Date(dtStart));
+ rawEnd = isoFormat.format(new Date(dtEnd));
+ }
+
+ CalendarEvent event = new CalendarEvent(title, rawStart, rawEnd, desc, loc);
+ formatEventForUI(event);
+ deviceEvents.add(event);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return deviceEvents;
+ }
+
+ // --- ROBUST DATO-PARSING (Løser "null" problemet) ---
+ public static void formatEventForUI(CalendarEvent event) {
+ if (event.getRawDate() == null) return;
+
+ SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault());
+ SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO"));
+ SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
+
+ outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
+ outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
+
+ try {
+ Date date = null;
+ Date endDate = null;
+ boolean isAllDay = false;
+
+ String raw = event.getRawDate();
+
+ // SJEKK 1: Er det heldagsdato? (Lengde 10, f.eks "2025-12-31")
+ if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) {
+ SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ date = shortFmt.parse(raw);
+ isAllDay = true;
+
+ if (event.getRawEndDate() != null && event.getRawEndDate().length() == 10) {
+ endDate = shortFmt.parse(event.getRawEndDate());
+ }
+ }
+ // SJEKK 2: Er det ISO format? (Har 'T')
+ else if (raw.contains("T")) {
+ SimpleDateFormat isoFmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
+ isoFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Viktig for Google events
+ date = isoFmt.parse(raw);
+
+ if (event.getRawEndDate() != null && event.getRawEndDate().contains("T")) {
+ endDate = isoFmt.parse(event.getRawEndDate());
+ }
+ }
+ // SJEKK 3: Er det SQL format? (Mellomrom)
+ else {
+ SimpleDateFormat sqlFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ sqlFmt.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
+ date = sqlFmt.parse(raw);
+
+ if (event.getRawEndDate() != null) {
+ endDate = sqlFmt.parse(event.getRawEndDate());
+ }
+ }
+
+ if (date != null) {
+ event.setDay(outputDay.format(date));
+ event.setMonth(outputMonth.format(date).toUpperCase());
+
+ if (isAllDay) {
+ event.setTime("Hele dagen");
+ } else {
+ String timeStr = outputTime.format(date);
+ if (endDate != null) {
+ // Hvis sluttdato er samme dag, vis bare klokkeslett
+ // (Enkelt sjekk: hvis datoene er like)
+ // Her forenkler vi og viser alltid sluttid hvis den finnes
+ timeStr += " - " + outputTime.format(endDate);
+ }
+ event.setTime("Kl. " + timeStr);
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ event.setDay("??");
+ event.setMonth("???");
+ event.setTime("Feil dato");
+ }
+ }
+
+ public static List mergeAndSort(List apiEvents, List deviceEvents) {
+ List all = new ArrayList<>(apiEvents);
+ all.addAll(deviceEvents);
+
+ Collections.sort(all, (e1, e2) -> {
+ String d1 = e1.getRawDate() != null ? e1.getRawDate() : "";
+ String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
+ return d1.compareTo(d2);
+ });
+
+ return all;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java
index 6f4eda9..b0f09e9 100644
--- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java
+++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java
@@ -1,198 +1,194 @@
-// FILSTI: app\src\main\java\com\kbs\kbsintranett\HomeFragment.java
package com.kbs.kbsintranett;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-
+import java.util.concurrent.TimeUnit;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class HomeFragment extends Fragment {
+ private ActivityResultLauncher requestPermissionLauncher;
+ private RecyclerView calendarRecycler;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Håndter svar på kalendertillatelse
+ requestPermissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestPermission(),
+ isGranted -> {
+ // Last kalender på nytt (nå med eller uten personlig kalender)
+ fetchCalendarEvents(calendarRecycler);
+ }
+ );
+
+ // Start bakgrunnsjobb for varsling (Hvert 15. minutt)
+ startNotificationWorker();
+ }
+
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- // Laster inn layouten fra XML (fragment_home.xml)
return inflater.inflate(R.layout.fragment_home, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- // ---------------------------------------------------------
- // 0. SETT OPP PROFIL-KNAPP (Ny!)
- // ---------------------------------------------------------
- // Vi finner ikonet vi la til i XML og sier at det skal gå til Profil-siden
+
+ // Profil-knapp
View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) {
- profileBtn.setOnClickListener(v -> {
- Navigation.findNavController(view).navigate(R.id.navigation_profile);
- });
+ profileBtn.setOnClickListener(v -> Navigation.findNavController(view).navigate(R.id.navigation_profile));
}
- // ---------------------------------------------------------
- // 1. SETT OPP KALENDER (Henter fra WordPress)
- // ---------------------------------------------------------
- RecyclerView calendarRecycler = view.findViewById(R.id.recycler_calendar);
+ // Kalender oppsett
+ calendarRecycler = view.findViewById(R.id.recycler_calendar);
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
+ calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
- // Starter henting av kalenderdata
- fetchCalendarEvents(calendarRecycler);
+ // "Se alle" knapp
+ 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));
+ }
- // ---------------------------------------------------------
- // 2. SETT OPP NYHETER (Hentes fra WordPress)
- // ---------------------------------------------------------
+ // 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);
+ }
+ }
+
+ // Nyheter
RecyclerView newsRecycler = view.findViewById(R.id.recycler_news);
newsRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
-
- // Gjør at scrollen flyter bedre inni NestedScrollView (hvis du bruker det i XML)
newsRecycler.setNestedScrollingEnabled(false);
- // Start henting av ekte data
+ newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>()));
fetchNewsFromWordpress(newsRecycler);
}
- /**
- * Henter kalenderhendelser fra WordPress via RetrofitClient
- */
private void fetchCalendarEvents(RecyclerView recyclerView) {
- // 1. Hent API-tjenesten vår
+ // 1. Hent personlige hendelser først
+ List deviceEvents = CalendarManager.getDeviceEvents(getContext());
+
WordPressApiService apiService = RetrofitClient.getApiService();
- // 2. Send forespørsel til nettet (Asynkront)
apiService.getCalendarEvents().enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
- if (getContext() == null || response.body() == null) return;
+ if (!isAdded()) return;
- List rawEvents = response.body();
- List formattedEvents = new ArrayList<>();
-
- // Formater for parsing og visning
- SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
- apiFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- SimpleDateFormat dayFormat = new SimpleDateFormat("dd", Locale.getDefault());
- dayFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- // Bruker norsk locale for måned (Jan, Feb, Mar, etc.)
- SimpleDateFormat monthFormat = new SimpleDateFormat("MMM", new Locale("no", "NO"));
- monthFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
- timeFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- for (CalendarEvent event : rawEvents) {
- try {
- // Bruker getRawDate() fra CalendarEvent.java (oppdatert)
- Date date = apiFormat.parse(event.getRawDate());
- String day = dayFormat.format(date);
- String month = monthFormat.format(date).toUpperCase(Locale.getDefault());
- String startTime = timeFormat.format(date);
-
- // Bruker den gamle konstruktøren for å sette formaterte data i adapteren
- formattedEvents.add(new CalendarEvent(
- event.getTitle(),
- startTime,
- day,
- month
- ));
- } catch (ParseException e) {
- e.printStackTrace();
- // Håndterer feil i parsing av dato/tid ved å vise rå data
- formattedEvents.add(new CalendarEvent(event.getTitle(), "Ukjent", event.getDay(), event.getMonth()));
+ List apiEvents = new ArrayList<>();
+ if (response.isSuccessful() && response.body() != null) {
+ for (CalendarEvent e : response.body()) {
+ CalendarManager.formatEventForUI(e); // Formatér datoer
+ apiEvents.add(e);
}
}
- recyclerView.setAdapter(new CalendarAdapter(formattedEvents));
+
+ // Flett lister
+ List merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
+
+ // Vis kun topp 5
+ List top5 = new ArrayList<>();
+ for(int i=0; i {
+ CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
+ sheet.show(getParentFragmentManager(), "CalendarDetails");
+ }));
}
@Override
public void onFailure(Call> call, Throwable t) {
- if (getContext() == null) return;
- System.err.println("Kalender Nettverksfeil: " + t.getMessage());
- // Vis feilmelding i RecyclerView
- List errorList = new ArrayList<>();
- errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverket ditt.", "00", "FEIL"));
- recyclerView.setAdapter(new CalendarAdapter(errorList));
+ if (!isAdded()) return;
+ // Hvis API feiler, vis bare personlige events hvis vi har noen
+ if (!deviceEvents.isEmpty()) {
+ List top5 = new ArrayList<>();
+ for(int i=0; i {
+ CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
+ sheet.show(getParentFragmentManager(), "CalendarDetails");
+ }));
+ } else {
+ // Vis feil hvis alt er tomt
+ List errorList = new ArrayList<>();
+ errorList.add(new CalendarEvent("Kunne ikke laste kalender", "Sjekk nettverk", "!", "OBS"));
+ recyclerView.setAdapter(new CalendarAdapter(errorList, null));
+ }
}
});
}
- /**
- * Henter nyheter fra WordPress via RetrofitClient
- */
+ private void startNotificationWorker() {
+ // Kjører en jobb hvert 15. minutt for å sjekke om det er nye møter
+ PeriodicWorkRequest notifRequest =
+ new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
+ .build();
+ WorkManager.getInstance(requireContext()).enqueue(notifRequest);
+ }
+
+ // ... (fetchNewsFromWordpress beholdes som før) ...
private void fetchNewsFromWordpress(RecyclerView recyclerView) {
- // 1. Hent API-tjenesten vår
+ // [Lim inn koden for nyheter fra forrige svar her, den var OK]
WordPressApiService apiService = RetrofitClient.getApiService();
- // 2. Send forespørsel til nettet (Asynkront)
apiService.getPosts().enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
- // Sjekk om appen fortsatt lever (viktig for å unngå krasj)
if (getContext() == null) return;
-
if (response.isSuccessful() && response.body() != null) {
- // 3. Suksess! Vi fikk data fra WordPress.
List wpPosts = response.body();
List newsList = new ArrayList<>();
+ java.text.SimpleDateFormat rawFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", java.util.Locale.getDefault());
+ rawFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Oslo"));
+ java.text.SimpleDateFormat targetFormat = new java.text.SimpleDateFormat("dd. MMM yyyy", java.util.Locale.getDefault());
+ targetFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Oslo"));
- // Datoformatering for nyhetene
- SimpleDateFormat rawFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
- rawFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- SimpleDateFormat targetFormat = new SimpleDateFormat("dd. MMM yyyy", Locale.getDefault());
- targetFormat.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // Fikser deprecation/tidssone
-
- // Konverter fra "WpPost" (API-format) til "NewsItem" (App-format)
for (WpPost post : wpPosts) {
String formattedDate = post.date;
-
try {
- // API-datoen (post.date) er i formatet "yyyy-MM-dd'T'HH:mm:ss"
- Date date = rawFormat.parse(post.date);
+ java.util.Date date = rawFormat.parse(post.date);
formattedDate = targetFormat.format(date);
- } catch (ParseException e) {
- System.err.println("Feil ved parsing av nyhetsdato: " + e.getMessage());
- }
-
- newsList.add(new NewsItem(
- post.getTitleStr(),
- post.getExcerptStr(),
- "Publisert: " + formattedDate
- ));
+ } catch (java.text.ParseException e) {}
+ newsList.add(new NewsItem(post.getTitleStr(), post.getExcerptStr(), "Publisert: " + formattedDate));
}
-
- // 4. Send listen til Adapteren slik at den vises på skjermen
- NewsAdapter adapter = new NewsAdapter(newsList);
- recyclerView.setAdapter(adapter);
- } else {
- System.err.println("Feil: Fikk svar, men noe var galt med dataene: " + response.code());
- // Her kunne vi vist en "Ingen nyheter"-tekst
- // (Løsningen har allerede lagt inn fallback i onFailure)
+ recyclerView.setAdapter(new NewsAdapter(newsList));
}
}
-
@Override
public void onFailure(Call> call, Throwable t) {
- // Nettverksfeil (Ingen nett, feil URL, etc)
if (getContext() == null) return;
- System.err.println("Nettverksfeil: " + t.getMessage());
- // Legg til en "Feilmelding" i listen så brukeren ser det
List errorList = new ArrayList<>();
errorList.add(new NewsItem("Kunne ikke laste nyheter", "Sjekk nettverket ditt.", "System"));
recyclerView.setAdapter(new NewsAdapter(errorList));
diff --git a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java
new file mode 100644
index 0000000..c0c5bf8
--- /dev/null
+++ b/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java
@@ -0,0 +1,99 @@
+package com.kbs.kbsintranett;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import retrofit2.Response;
+
+public class NotificationWorker extends Worker {
+
+ private static final String CHANNEL_ID = "kbs_calendar_channel";
+ private static final String PREFS_NAME = "KBSNotificationPrefs";
+
+ public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ // Dette kjører i bakgrunnen
+ try {
+ // Hent events synkront (ikke enqueue)
+ Response> response = RetrofitClient.getApiService().getCalendarEvents().execute();
+
+ if (response.isSuccessful() && response.body() != null) {
+ checkAndNotify(response.body());
+ return Result.success();
+ } else {
+ return Result.retry();
+ }
+ } catch (IOException e) {
+ return Result.retry();
+ }
+ }
+
+ private void checkAndNotify(List events) {
+ SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ long now = System.currentTimeMillis();
+ long fifteenMinutes = 15 * 60 * 1000;
+
+ 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());
+
+ for (CalendarEvent event : events) {
+ try {
+ Date eventDate;
+ if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate());
+ else eventDate = sqlFormat.parse(event.getRawDate());
+
+ if (eventDate == null) continue;
+
+ long diff = eventDate.getTime() - now;
+
+ // Hvis eventet starter innen de neste 30 min, og ikke allerede varslet
+ if (diff > 0 && diff < (30 * 60 * 1000)) {
+ String eventId = event.getTitle() + event.getRawDate(); // Enkel ID
+ boolean alreadyNotified = prefs.getBoolean(eventId, false);
+
+ if (!alreadyNotified) {
+ sendNotification(event.getTitle(), "Starter kl " + event.getTime());
+ // Lagre at vi har varslet
+ prefs.edit().putBoolean(eventId, true).apply();
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void sendNotification(String title, String content) {
+ NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
+ manager.createNotificationChannel(channel);
+ }
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her
+ .setContentTitle(title)
+ .setContentText(content)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setAutoCancel(true);
+
+ manager.notify((int) System.currentTimeMillis(), builder.build());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java
index c75de16..de6008e 100644
--- a/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java
+++ b/app/src/main/java/com/kbs/kbsintranett/RetrofitClient.java
@@ -11,32 +11,30 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
+import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitClient {
private static final String BASE_URL = "https://intranet.kbs.no/";
-
- // VI FJERNER FAKE_COOKIE HERFRA! Den trengs ikke lenger.
-
private static Retrofit retrofit = null;
public static WordPressApiService getApiService() {
- // Vi må bygge klienten på nytt hvis vi logger ut/inn, men for enkelhets skyld
- // sjekker vi bare null her.
if (retrofit == null) {
+ // NYTT: Logging Interceptor
+ HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
+ logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers)
+
OkHttpClient client = new OkHttpClient.Builder()
+ .addInterceptor(logging) // Legg til loggingen her
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
- // 1. Hent cookie fra UserManager
String dynamicCookie = UserManager.getInstance().getCookie();
-
- // 2. Hvis vi har en cookie, legg den til i headeren
if (dynamicCookie != null && !dynamicCookie.isEmpty()) {
builder.header("Cookie", dynamicCookie);
}
@@ -48,6 +46,7 @@ public class RetrofitClient {
Gson gson = new GsonBuilder()
.registerTypeAdapter(new TypeToken>(){}.getType(), new ChoicesAdapter())
+ .setLenient() // NYTT: Gjør parsing litt mer tilgivende
.create();
retrofit = new Retrofit.Builder()
@@ -59,7 +58,6 @@ public class RetrofitClient {
return retrofit.create(WordPressApiService.class);
}
- // Hjelpemetode for å nullstille Retrofit ved utlogging
public static void clearClient() {
retrofit = null;
}
diff --git a/app/src/main/res/layout/bottom_sheet_calendar_details.xml b/app/src/main/res/layout/bottom_sheet_calendar_details.xml
new file mode 100644
index 0000000..154d71c
--- /dev/null
+++ b/app/src/main/res/layout/bottom_sheet_calendar_details.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_calendar_full.xml b/app/src/main/res/layout/fragment_calendar_full.xml
new file mode 100644
index 0000000..f6c9437
--- /dev/null
+++ b/app/src/main/res/layout/fragment_calendar_full.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 76853e9..38a2696 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -5,38 +5,61 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
- android:background="@color/kbs_very_light_blue">
+ android:background="@color/kbs_very_light_blue">
-
-
+ android:layout_marginBottom="16dp"
+ android:paddingHorizontal="8dp">
-
+
+
+
+
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp">
+
+
+
+
+
+ tools:layout="@layout/fragment_home">
+
+
+
+