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 @@ + + + + + + + + + + + +