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