diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt
new file mode 100644
index 0000000..90203e0
--- /dev/null
+++ b/hele_prosjektet.txt
@@ -0,0 +1,11189 @@
+Dette er kildekoden til et Android Studio-prosjekt.
+Hver fil er separert med overskrifter.
+
+
+============================================================
+FILSTI: build.gradle.kts
+============================================================
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ // NY LINJE: Legg til Google Services plugin her
+ id("com.google.gms.google-services") version "4.4.2" apply false
+}
+
+
+============================================================
+FILSTI: settings.gradle.kts
+============================================================
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "KBS Intranett"
+include(":app")
+
+
+============================================================
+FILSTI: app\build.gradle.kts
+============================================================
+import java.util.Properties
+import java.io.FileInputStream
+
+plugins {
+ alias(libs.plugins.android.application)
+ // NY LINJE: Aktiver Google Services plugin her
+ id("com.google.gms.google-services")
+}
+
+// --- NY KODE: Last inn local.properties ---
+// Vi bruker "Properties()" direkte siden vi har importert den på toppen
+val localProperties = Properties()
+val localPropertiesFile = rootProject.file("local.properties")
+if (localPropertiesFile.exists()) {
+ localProperties.load(FileInputStream(localPropertiesFile))
+}
+// ------------------------------------------
+
+android {
+ namespace = "com.kbs.kbsintranett"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.kbs.kbsintranett"
+ minSdk = 28
+ targetSdk = 34
+ versionCode = 4
+ versionName = "1.5.1"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ // Hent verdien vi lastet inn på toppen av filen
+ val webClientId = localProperties.getProperty("WEB_CLIENT_ID") ?: ""
+
+ // Opprett BuildConfig-feltet.
+ // Vi legger på ekstra hermetegn (\") for at det skal bli en String i Java-koden.
+ buildConfigField("String", "WEB_CLIENT_ID", "\"$webClientId\"")
+ }
+
+ // NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden
+ buildFeatures {
+ buildConfig = true
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ implementation(libs.appcompat)
+ implementation(libs.material)
+ implementation(libs.activity)
+ implementation(libs.constraintlayout)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+
+ // Nettverk og JSON-håndtering
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+ implementation("com.google.code.gson:gson:2.10.1")
+
+ // Navigation Component
+ val navVersion = "2.8.5"
+ implementation("androidx.navigation:navigation-fragment:$navVersion")
+ implementation("androidx.navigation:navigation-ui:$navVersion")
+
+ 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")
+
+ // Swipe Refresh Layout
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+
+ // NY LINJE: Firebase BOM (Bill of Materials) styrer versjoner
+ implementation(platform("com.google.firebase:firebase-bom:33.1.2"))
+
+ // NY LINJE: (Valgfritt, men lurt for statistikk)
+ implementation("com.google.firebase:firebase-analytics")
+
+ // NYTT: Firebase Cloud Messaging lagt til her
+ implementation("com.google.firebase:firebase-messaging")
+}
+
+============================================================
+FILSTI: app\proguard-rules.pro
+============================================================
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+============================================================
+FILSTI: app\src\androidTest\java\com\kbs\kbsintranett\ExampleInstrumentedTest.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.kbs.kbsintranett", appContext.getPackageName());
+ }
+}
+
+============================================================
+FILSTI: app\src\main\AndroidManifest.xml
+============================================================
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\AddTaskBottomSheet.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.app.AlertDialog;
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class AddTaskBottomSheet extends BottomSheetDialogFragment {
+
+ private EditText etTitle, etDesc;
+ private Button btnDate, btnUsers, btnSave, btnClearDate; // NYTT
+ private TextView txtSheetTitle, txtDatePreview, txtUsersPreview;
+
+ private Calendar dueDate = Calendar.getInstance();
+ private boolean hasDate = true; // NYTT
+
+ private List filteredUsers = new ArrayList<>();
+ private List selectedUsers = new ArrayList<>();
+ private TaskItem taskToEdit = null;
+
+ public interface OnTaskAddedListener {
+ void onTaskAdded(TaskItem task);
+ void onTaskUpdated(TaskItem task);
+ }
+
+ private OnTaskAddedListener listener;
+
+ public void setOnTaskAddedListener(OnTaskAddedListener listener) {
+ this.listener = listener;
+ }
+
+ public void setTaskToEdit(TaskItem task) {
+ this.taskToEdit = task;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
+ dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+ dialog.setOnShowListener(dialogInterface -> {
+ BottomSheetDialog d = (BottomSheetDialog) dialogInterface;
+ View bottomSheet = d.findViewById(com.google.android.material.R.id.design_bottom_sheet);
+ if (bottomSheet != null) {
+ BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED);
+ }
+ });
+ return dialog;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.bottom_sheet_add_task, container, false);
+
+ txtSheetTitle = v.findViewById(R.id.txt_sheet_title);
+ etTitle = v.findViewById(R.id.et_task_title);
+ etDesc = v.findViewById(R.id.et_task_desc);
+ btnDate = v.findViewById(R.id.btn_task_date);
+ btnClearDate = v.findViewById(R.id.btn_clear_date); // NYTT
+ btnUsers = v.findViewById(R.id.btn_task_users);
+ btnSave = v.findViewById(R.id.btn_save_task);
+ txtDatePreview = v.findViewById(R.id.txt_date_preview);
+ txtUsersPreview = v.findViewById(R.id.txt_users_preview);
+
+ if (taskToEdit != null) {
+ txtSheetTitle.setText("Rediger Oppgave");
+ etTitle.setText(taskToEdit.getTitle());
+ etDesc.setText(taskToEdit.getDescription());
+
+ if (taskToEdit.getDueDate() > 0) {
+ dueDate.setTimeInMillis(taskToEdit.getDueDate());
+ hasDate = true;
+ } else {
+ hasDate = false;
+ }
+ btnSave.setText("Oppdater Oppgave");
+ } else {
+ dueDate.add(Calendar.DAY_OF_MONTH, 1);
+ hasDate = true;
+ }
+
+ updateDatePreview();
+
+ btnDate.setOnClickListener(view -> {
+ new DatePickerDialog(getContext(), (d, y, m, day) -> {
+ dueDate.set(y, m, day);
+ hasDate = true;
+ updateDatePreview();
+ }, dueDate.get(Calendar.YEAR), dueDate.get(Calendar.MONTH), dueDate.get(Calendar.DAY_OF_MONTH)).show();
+ });
+
+ // NYTT: Knapp for å fjerne frist
+ btnClearDate.setOnClickListener(v1 -> {
+ hasDate = false;
+ updateDatePreview();
+ });
+
+ btnUsers.setOnClickListener(view -> showUserSelectionDialog());
+ btnSave.setOnClickListener(view -> saveTask());
+
+ fetchAndFilterUsers();
+
+ return v;
+ }
+
+ private void fetchAndFilterUsers() {
+ RetrofitClient.getApiService().getUsersList().enqueue(new Callback>() {
+ @Override
+ public void onResponse(Call> call, Response> response) {
+ if (response.isSuccessful() && response.body() != null) {
+ filteredUsers = UserFilterHelper.getFilteredUsers(response.body());
+
+ if (taskToEdit != null) {
+ selectedUsers.clear();
+ Map currentAssignees = taskToEdit.getAssigneeStatus();
+ for (User u : filteredUsers) {
+ if (currentAssignees.containsKey(u.getEmail())) {
+ selectedUsers.add(u);
+ }
+ }
+ updateUsersPreview();
+ }
+ }
+ }
+ @Override
+ public void onFailure(Call> call, Throwable t) {}
+ });
+ }
+
+ private void showUserSelectionDialog() {
+ if (filteredUsers.isEmpty()) {
+ Toast.makeText(getContext(), "Henter tilgjengelige personer...", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ String[] names = new String[filteredUsers.size()];
+ boolean[] checked = new boolean[filteredUsers.size()];
+ for (int i = 0; i < filteredUsers.size(); i++) {
+ names[i] = filteredUsers.get(i).getName();
+ boolean isSelected = false;
+ for(User su : selectedUsers) {
+ if(su.getEmail().equalsIgnoreCase(filteredUsers.get(i).getEmail())) {
+ isSelected = true;
+ break;
+ }
+ }
+ checked[i] = isSelected;
+ }
+ new AlertDialog.Builder(getContext())
+ .setTitle("Tildel til...")
+ .setMultiChoiceItems(names, checked, (dialog, which, isChecked) -> {
+ User user = filteredUsers.get(which);
+ if (isChecked) {
+ selectedUsers.add(user);
+ } else {
+ selectedUsers.removeIf(u -> u.getEmail().equalsIgnoreCase(user.getEmail()));
+ }
+ })
+ .setPositiveButton("OK", (dialog, which) -> updateUsersPreview())
+ .show();
+ }
+
+ private void updateUsersPreview() {
+ if (selectedUsers.isEmpty()) txtUsersPreview.setText("Kun meg");
+ else if (selectedUsers.size() == 1) txtUsersPreview.setText(selectedUsers.get(0).getName());
+ else txtUsersPreview.setText(selectedUsers.size() + " personer valgt");
+ }
+
+ private void updateDatePreview() {
+ if (hasDate) {
+ SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault());
+ txtDatePreview.setText("Frist: " + sdf.format(dueDate.getTime()));
+ btnClearDate.setVisibility(View.VISIBLE);
+ } else {
+ txtDatePreview.setText("Ingen frist");
+ btnClearDate.setVisibility(View.GONE);
+ }
+ }
+
+ private void saveTask() {
+ String title = etTitle.getText().toString().trim();
+ if (title.isEmpty()) {
+ etTitle.setError("Mangler tittel");
+ return;
+ }
+
+ long finalDueDate = hasDate ? dueDate.getTimeInMillis() : 0;
+
+ if (taskToEdit != null) {
+ Map oldStatus = taskToEdit.getAssigneeStatus();
+ taskToEdit.getAssigneeStatus().clear();
+
+ if (selectedUsers.isEmpty()) {
+ String myEmail = UserManager.getInstance().getUserEmail();
+ taskToEdit.addAssignee(myEmail);
+ if (oldStatus.containsKey(myEmail)) taskToEdit.setParticipantStatus(myEmail, oldStatus.get(myEmail));
+ } else {
+ for (User u : selectedUsers) {
+ taskToEdit.addAssignee(u.getEmail());
+ if (oldStatus.containsKey(u.getEmail())) {
+ taskToEdit.setParticipantStatus(u.getEmail(), oldStatus.get(u.getEmail()));
+ }
+ }
+ }
+ taskToEdit.setTitle(title);
+ taskToEdit.setDescription(etDesc.getText().toString());
+ taskToEdit.setDueDate(finalDueDate);
+ if (listener != null) listener.onTaskUpdated(taskToEdit);
+ } else {
+ TaskItem newTask = new TaskItem(title, etDesc.getText().toString(), finalDueDate);
+ if (selectedUsers.isEmpty()) {
+ newTask.addAssignee(UserManager.getInstance().getUserEmail());
+ } else {
+ for (User u : selectedUsers) newTask.addAssignee(u.getEmail());
+ }
+ if (listener != null) listener.onTaskAdded(newTask);
+ }
+ dismiss();
+ }
+}
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.Manifest;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import androidx.core.app.ActivityCompat;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat; // <-- Denne manglet
+
+public class AlarmReceiver extends BroadcastReceiver {
+
+ private static final String CHANNEL_ID = "kbs_calendar_channel";
+ private static final String CHANNEL_NAME = "KBS Kalendervarsler";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String title = intent.getStringExtra("TITLE");
+ String message = intent.getStringExtra("MESSAGE");
+ int notificationId = intent.getIntExtra("ID", 0);
+
+ createNotificationChannel(context);
+ showNotification(context, title, message, notificationId);
+ }
+
+ private void showNotification(Context context, String title, String message, int notificationId) {
+ // Sjekk rettigheter for Android 13+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ // Vi kan ikke vise varsel uten rettighet.
+ return;
+ }
+ }
+
+ // Intent for hva som skjer når man trykker på varselet (åpne appen)
+ Intent tapIntent = new Intent(context, MainActivity.class);
+ tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ tapIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ );
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_stat_kbs)
+ .setColor(ContextCompat.getColor(context, R.color.kbs_logo_blue)) // Setter KBS-blå farge på ikonet
+ .setContentTitle(title)
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true);
+
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ notificationManager.notify(notificationId, builder.build());
+ }
+
+ private void createNotificationChannel(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ int importance = NotificationManager.IMPORTANCE_HIGH;
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
+ channel.setDescription("Varsler for kalenderhendelser i KBS Intranett");
+
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+ }
+}
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmScheduler.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.util.Log;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class AlarmScheduler {
+
+ private static final String TAG = "AlarmScheduler";
+ private static final String PREFS_NAME = "kbs_alarm_history";
+
+ /**
+ * Denne metoden går gjennom en liste hendelser og setter alarmer for dem.
+ */
+ public static void scheduleAlarmsForEvents(Context context, List events) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+ // Sjekk rettigheter for Android 12+ (Exact Alarm)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
+ Log.w(TAG, "Mangler rettighet til å sette nøyaktige alarmer.");
+ return;
+ }
+
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ long now = System.currentTimeMillis();
+ SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
+
+ // Vi ser etter hendelser 30 dager frem i tid
+ long futureWindow = now + (30L * 24 * 60 * 60 * 1000L);
+
+ for (CalendarEvent event : events) {
+ try {
+ // Hopp over hvis ingen dato eller heldags (uten tidspunkt)
+ if (event.getRawDate() == null || event.getRawDate().length() == 10) continue;
+
+ Date eventDate = null;
+ if (event.getRawDate().contains("T")) {
+ String raw = event.getRawDate();
+ if (raw.length() > 19) raw = raw.substring(0, 19);
+ eventDate = isoFormat.parse(raw);
+ }
+
+ if (eventDate == null) continue;
+
+ // Loop gjennom alle varsler (f.eks. 15 min før, 60 min før)
+ for (int minutesBefore : event.getReminders()) {
+ if (minutesBefore < 0) continue;
+
+ long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L);
+ String alarmKey = "alarm_" + event.getId() + "_" + triggerTime;
+
+ // Hvis tidspunktet er i fremtiden (og innenfor vinduet)
+ if (triggerTime > now && triggerTime < futureWindow) {
+
+ // Sjekk om vi allerede har satt denne alarmen for å unngå dobbeltarbeid
+ if (prefs.getBoolean(alarmKey, false)) {
+ continue;
+ }
+
+ int alarmId = alarmKey.hashCode(); // Unik ID basert på hendelse+tid
+
+ Intent intent = new Intent(context, AlarmReceiver.class);
+ intent.putExtra("TITLE", event.getTitle());
+ String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate);
+ intent.putExtra("MESSAGE", "Starter kl " + timeStr);
+ intent.putExtra("ID", alarmId);
+
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ context,
+ alarmId,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
+ );
+
+ // Sett alarmen
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
+ } else {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
+ }
+
+ // Marker som satt
+ prefs.edit().putBoolean(alarmKey, true).apply();
+ Log.d(TAG, "Alarm satt for " + event.getTitle() + " om " + minutesBefore + " min.");
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Feil ved setting av alarm", e);
+ }
+ }
+ }
+}
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
+============================================================
+// FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
+package com.kbs.kbsintranett;
+
+import android.util.Log;
+import com.google.gson.JsonElement;
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+public class AuthRepository {
+
+ private static final String TAG = "AuthRepository";
+
+ // Interface for å gi beskjed tilbake til Activity/Fragment
+ public interface AuthCallback {
+ void onSuccess(String role);
+ void onError(String message);
+ }
+
+ /**
+ * Utfører selve API-kallet mot WordPress.
+ * Denne brukes nå av både MainActivity (Silent Sign-In) og LoginFragment (Manuell).
+ */
+ public static void loginToWordPress(String googleIdToken, String displayName, String email, String photoUrl, AuthCallback callback) {
+
+ // 1. Lagre Google-info midlertidig
+ UserManager.getInstance().setUserData(displayName, email, googleIdToken, photoUrl);
+
+ // 2. Gjør klar request
+ LoginRequest request = new LoginRequest(googleIdToken);
+
+ // 3. Send til WordPress
+ RetrofitClient.getApiService().googleLogin(request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful() && response.body() != null && response.body().success) {
+ // SUKSESS!
+ String cookie = response.body().fullCookie;
+ String role = response.body().role;
+
+ // NYTT: Hent utvidet info fra responsen
+ int userId = response.body().userId;
+ String fName = response.body().firstName;
+ String lName = response.body().lastName;
+ String stilling = response.body().stilling;
+ String mobil = response.body().mobiltelefon;
+
+ Log.d(TAG, "WordPress Login suksess! Rolle: " + role + ", UserID: " + userId);
+
+ // Lagre cookie, rolle og ID
+ UserManager.getInstance().setCookie(cookie);
+ UserManager.getInstance().setUserRole(role);
+ UserManager.getInstance().setUserId(userId);
+
+ // Lagre utvidet info i UserManager
+ UserManager.getInstance().setExtendedUserInfo(fName, lName, stilling, mobil);
+
+ // Lagre listen over skrivbare kalendere
+ UserManager.getInstance().setWriteableCalendars(response.body().writeableCalendars);
+
+ // NYTT: Hvis vi har en ventende FCM-token, send den nå som vi er logget inn
+ String pendingToken = UserManager.getInstance().getFcmToken();
+ if (pendingToken != null && !pendingToken.isEmpty()) {
+ updateDeviceToken(pendingToken);
+ }
+
+ callback.onSuccess(role);
+
+ } else {
+ Log.e(TAG, "WordPress Login nektet. Kode: " + response.code());
+ callback.onError("Kunne ikke logge inn på Intranettet (Kode: " + response.code() + ")");
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e(TAG, "Nettverksfeil mot WP", t);
+ callback.onError("Nettverksfeil: " + t.getMessage());
+ }
+ });
+ }
+
+ /**
+ * Sender FCM-token til WordPress for å registrere enheten for push-varsler.
+ */
+ public static void updateDeviceToken(String token) {
+ if (!UserManager.getInstance().isLoggedIn()) {
+ // Hvis ikke logget inn, bare lagre den til senere
+ UserManager.getInstance().setFcmToken(token);
+ return;
+ }
+
+ // Send til server
+ RegisterDeviceRequest request = new RegisterDeviceRequest(token);
+ RetrofitClient.getApiService().registerDevice(request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (response.isSuccessful()) {
+ Log.d(TAG, "FCM Token registrert på server OK.");
+ } else {
+ Log.e(TAG, "Feil ved registrering av FCM Token. Kode: " + response.code());
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ Log.e(TAG, "Nettverksfeil ved sending av FCM token", t);
+ }
+ });
+ }
+}
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\CacheManager.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.content.Context;
+import android.content.SharedPreferences; // NYTT
+import android.util.Log;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CacheManager {
+ private static final String FILE_CALENDAR = "cache_calendar.json";
+ private static final String FILE_NEWS = "cache_news.json";
+ private static final String FILE_HANDBOOK = "cache_handbook.json";
+ private static final String FILE_TASKS = "cache_tasks.json";
+
+ private static final String TAG = "CacheManager";
+ private static final Gson gson = new Gson();
+
+ // NYTT: SharedPreferences for tidsstempler
+ private static final String PREFS_CACHE = "kbs_cache_prefs";
+
+ // --- KALENDER ---
+ public static void saveCalendarEvents(Context context, List events) {
+ saveList(context, FILE_CALENDAR, events);
+ setLastFetchTime(context, "calendar"); // NYTT
+ }
+
+ public static List getCachedCalendarEvents(Context context) {
+ Type type = new TypeToken>() {}.getType();
+ List list = loadList(context, FILE_CALENDAR, type);
+ return list != null ? list : new ArrayList<>();
+ }
+
+ // --- NYHETER ---
+ public static void saveNewsPosts(Context context, List posts) {
+ saveList(context, FILE_NEWS, posts);
+ setLastFetchTime(context, "news"); // NYTT
+ }
+
+ public static List getCachedNewsPosts(Context context) {
+ Type type = new TypeToken>() {}.getType();
+ List list = loadList(context, FILE_NEWS, type);
+ return list != null ? list : new ArrayList<>();
+ }
+
+ // --- HÅNDBOK ---
+ public static void saveHandbookItems(Context context, List items) {
+ saveList(context, FILE_HANDBOOK, items);
+ // Håndboken endres sjelden, trenger kanskje ikke tidssjekk, men greit å ha
+ }
+
+ public static List getCachedHandbookItems(Context context) {
+ Type type = new TypeToken>() {}.getType();
+ List list = loadList(context, FILE_HANDBOOK, type);
+ return list != null ? list : new ArrayList<>();
+ }
+
+ // --- OPPGAVER ---
+ public static void saveTasks(Context context, List tasks) {
+ saveList(context, FILE_TASKS, tasks);
+ setLastFetchTime(context, "tasks"); // NYTT
+ }
+
+ public static List getTasks(Context context) {
+ Type type = new TypeToken>() {}.getType();
+ List list = loadList(context, FILE_TASKS, type);
+ return list != null ? list : new ArrayList<>();
+ }
+
+ // --- LOGIKK FOR TID ---
+
+ private static void setLastFetchTime(Context context, String key) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
+ prefs.edit().putLong(key + "_timestamp", System.currentTimeMillis()).apply();
+ }
+
+ /**
+ * Sjekker om cachen er gyldig.
+ * @param maxAgeMinutes Hvor gammel cachen kan være før vi henter på nytt (f.eks. 60 minutter).
+ * Hvis push fungerer, kan vi sette denne høyt!
+ */
+ public static boolean isCacheValid(Context context, String key, int maxAgeMinutes) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
+ long lastTime = prefs.getLong(key + "_timestamp", 0);
+ long diff = System.currentTimeMillis() - lastTime;
+ // Konverter minutter til millisekunder
+ long maxDiff = maxAgeMinutes * 60 * 1000L;
+
+ return diff < maxDiff;
+ }
+
+ // Metode for å tvinge oppdatering neste gang (brukes av FCM)
+ public static void invalidateCache(Context context, String key) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_CACHE, Context.MODE_PRIVATE);
+ prefs.edit().remove(key + "_timestamp").apply();
+ }
+
+ // --- GENERISKE HJELPEMETODER (Uendret) ---
+
+ private static void saveList(Context context, String filename, List list) {
+ if (context == null || list == null) return;
+ new Thread(() -> {
+ try {
+ String json = gson.toJson(list);
+ FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
+ fos.write(json.getBytes());
+ fos.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Feil ved lagring av cache: " + filename, e);
+ }
+ }).start();
+ }
+
+ private static List loadList(Context context, String filename, Type type) {
+ if (context == null) return null;
+ File file = new File(context.getFilesDir(), filename);
+ if (!file.exists()) return null;
+
+ try {
+ FileInputStream fis = context.openFileInput(filename);
+ InputStreamReader isr = new InputStreamReader(fis);
+ BufferedReader bufferedReader = new BufferedReader(isr);
+ StringBuilder sb = new StringBuilder();
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line);
+ }
+ fis.close();
+ return gson.fromJson(sb.toString(), type);
+ } catch (Exception e) {
+ Log.e(TAG, "Feil ved lesing av cache: " + filename, e);
+ return null;
+ }
+ }
+}
+
+============================================================
+FILSTI: app\src\main\java\com\kbs\kbsintranett\CalendarAdapter.java
+============================================================
+package com.kbs.kbsintranett;
+
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import java.util.List;
+
+public class CalendarAdapter extends RecyclerView.Adapter {
+
+ public static final int TYPE_EVENT = 0;
+ public static final int TYPE_YEAR_HEADER = 1;
+
+ private List