Før individuelle avtaler

This commit is contained in:
ErolHaagenrud 2025-12-19 09:56:51 +01:00
parent 864820212f
commit 93092f33d9
11 changed files with 399 additions and 168 deletions

View file

@ -7,56 +7,38 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
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 TAG = "NotificationWorker";
public class AlarmScheduler {
private static final String TAG = "AlarmScheduler";
private static final String PREFS_NAME = "kbs_alarm_history";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
try {
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
if (response.isSuccessful() && response.body() != null) {
scheduleAlarms(response.body());
return Result.success();
} else {
if (response.code() >= 400 && response.code() < 500) return Result.failure();
return Result.retry();
}
} catch (IOException e) {
return Result.retry();
}
}
private void scheduleAlarms(List<CalendarEvent> events) {
Context context = getApplicationContext();
/**
* Denne metoden går gjennom en liste hendelser og setter alarmer for dem.
*/
public static void scheduleAlarmsForEvents(Context context, List<CalendarEvent> 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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) return;
long now = System.currentTimeMillis();
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
long catchUpWindow = now - (30 * 60 * 1000L);
long futureWindow = now + (30 * 24 * 60 * 60 * 1000L); // 30 dager frem
// 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;
@ -65,28 +47,26 @@ public class NotificationWorker extends Worker {
if (raw.length() > 19) raw = raw.substring(0, 19);
eventDate = isoFormat.parse(raw);
}
if (eventDate == null) continue;
// Loop gjennom alle varsler for denne hendelsen
// Loop gjennom alle varsler (f.eks. 15 min før, 60 min før)
for (int minutesBefore : event.getReminders()) {
if (minutesBefore < 0) continue; // 0 betyr "ved start", negative ignoreres
if (minutesBefore < 0) continue;
long triggerTime = eventDate.getTime() - (minutesBefore * 60 * 1000L);
// Unik nøkkel for denne alarmen: EventID + Tidspunkt
String alarmKey = "alarm_" + event.getId() + "_" + triggerTime;
// Sjekk om vi allerede har fyrt denne alarmen
// 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; // Allerede håndtert
continue;
}
if (triggerTime > catchUpWindow && triggerTime < futureWindow) {
if (triggerTime < now) {
triggerTime = now + 1000; // Catch-up
}
int alarmId = alarmKey.hashCode(); // Unik ID basert hendelse+tid
int alarmId = alarmKey.hashCode();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("TITLE", event.getTitle());
String timeStr = new SimpleDateFormat("HH:mm", Locale.getDefault()).format(eventDate);
@ -94,9 +74,13 @@ public class NotificationWorker extends Worker {
intent.putExtra("ID", alarmId);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
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 {
@ -105,14 +89,12 @@ public class NotificationWorker extends Worker {
// Marker som satt
prefs.edit().putBoolean(alarmKey, true).apply();
Log.i(TAG, "Satt alarm: " + event.getTitle() + " (" + minutesBefore + "m før)");
Log.d(TAG, "Alarm satt for " + event.getTitle() + " om " + minutesBefore + " min.");
}
}
} catch (Exception e) {
Log.e(TAG, "Feil", e);
Log.e(TAG, "Feil ved setting av alarm", e);
}
}
// Rensk opp gamle nøkler (valgfritt, for å spare plass over tid)
}
}

View file

@ -2,6 +2,7 @@
package com.kbs.kbsintranett;
import android.util.Log;
import com.google.gson.JsonElement;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -57,6 +58,12 @@ public class AuthRepository {
// Lagre listen over skrivbare kalendere
UserManager.getInstance().setWriteableCalendars(response.body().writeableCalendars);
// NYTT: Hvis vi har en ventende FCM-token, send den som vi er logget inn
String pendingToken = UserManager.getInstance().getFcmToken();
if (pendingToken != null && !pendingToken.isEmpty()) {
updateDeviceToken(pendingToken);
}
callback.onSuccess(role);
} else {
@ -72,4 +79,33 @@ public class AuthRepository {
}
});
}
/**
* 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<JsonElement>() {
@Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> 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<JsonElement> call, Throwable t) {
Log.e(TAG, "Nettverksfeil ved sending av FCM token", t);
}
});
}
}

View file

@ -8,6 +8,7 @@ import java.util.List;
public class CalendarEvent implements Serializable {
@SerializedName("id")
private String id;
@SerializedName("title")
private String title;
@ -66,6 +67,8 @@ public class CalendarEvent implements Serializable {
public void setCalendarColor(String color) { this.calendarColor = color; }
// --- KOMPATIBILITETS-METODER ---
// Denne brukes for enkle varsler
public void setReminderMinutes(int minutes) {
this.reminders = new ArrayList<>();
if (minutes > 0) {
@ -73,6 +76,12 @@ public class CalendarEvent implements Serializable {
}
}
// NY METODE (Den som manglet og forårsaket krasj)
// Lar oss sette hele listen med varsler en gang
public void setRemindersList(List<Integer> reminders) {
this.reminders = reminders != null ? new ArrayList<>(reminders) : new ArrayList<>();
}
public int getReminderMinutes() {
if (reminders != null && !reminders.isEmpty()) {
return reminders.get(0);

View file

@ -3,6 +3,7 @@ package com.kbs.kbsintranett;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
@ -33,8 +34,6 @@ import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -117,7 +116,6 @@ public class CreateEventFragment extends Fragment {
spinnerRecurrence.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// Unngå å overskrive ved innlasting hvis vi allerede har satt en verdi
if (eventToEdit != null && position == 0 && selectedRRule != null) return;
String selected = parent.getItemAtPosition(position).toString();
@ -140,7 +138,6 @@ public class CreateEventFragment extends Fragment {
etDesc.setText(cleanDesc);
etLocation.setText(event.getLocation());
// --- FIKS 404 FEIL VED OPPDATERING ---
ArrayAdapter<String> adapter = (ArrayAdapter<String>) spinnerCalendar.getAdapter();
if (adapter != null) {
int position = adapter.getPosition(event.getCalendarName());
@ -149,7 +146,6 @@ public class CreateEventFragment extends Fragment {
}
}
spinnerCalendar.setEnabled(false);
// -------------------------------------
try {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
@ -210,17 +206,14 @@ public class CreateEventFragment extends Fragment {
}
}
// --- NY LOGIKK FOR FARGER I SPINNER ---
private String getCalendarColor(String name) {
// Matcher fargene i PHP-config (V12.6)
switch (name) {
case "Felles": return "#0069B3"; // KBS Blå
case "Administrasjonen": return "#607D8B"; // Blue Grey
case "Serviceavdelingen": return "#E65100"; // Orange
case "Automasjonsavdelingen": return "#2E7D32"; // Green
case "Prosjektavdelingen": return "#7B1FA2"; // Purple
default: return "#888888"; // Grå fallback
case "Felles": return "#0069B3";
case "Administrasjonen": return "#607D8B";
case "Serviceavdelingen": return "#E65100";
case "Automasjonsavdelingen": return "#2E7D32";
case "Prosjektavdelingen": return "#7B1FA2";
default: return "#888888";
}
}
@ -228,41 +221,27 @@ public class CreateEventFragment extends Fragment {
List<String> calendars = UserManager.getInstance().getWriteableCalendars();
if (calendars.isEmpty()) calendars.add("Felles");
// Vi bruker en Custom Adapter for å styre farger
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, calendars) {
// getView: Dette er det som vises i selve boksen når noe er valgt
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
TextView view = (TextView) super.getView(position, convertView, parent);
String calName = getItem(position);
String colorHex = getCalendarColor(calName);
// Sett bakgrunnsfarge lik kalenderfarge
view.setBackgroundColor(Color.parseColor(colorHex));
// Hvit tekst for kontrast
view.setTextColor(Color.WHITE);
view.setTypeface(null, Typeface.BOLD);
return view;
}
// getDropDownView: Dette er listen som popper opp
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
TextView view = (TextView) super.getDropDownView(position, convertView, parent);
String calName = getItem(position);
String colorHex = getCalendarColor(calName);
// Her holder vi bakgrunnen hvit, men farger teksten
view.setBackgroundColor(Color.WHITE);
view.setTextColor(Color.parseColor(colorHex));
view.setTypeface(null, Typeface.BOLD);
return view;
}
};
@ -270,7 +249,6 @@ public class CreateEventFragment extends Fragment {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCalendar.setAdapter(adapter);
}
// --------------------------------------
private void setupReminderChips() {
addChip("Ved start", 0);
@ -539,16 +517,15 @@ public class CreateEventFragment extends Fragment {
String format = isAllDay ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm";
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
String startTimeStr = sdf.format(startCal.getTime());
String endTimeStr = sdf.format(endCal.getTime());
String location = etLocation.getText().toString();
String description = etDesc.getText().toString();
List<Integer> reminders = getSelectedReminders();
CreateEventRequest req = new CreateEventRequest(
title,
etDesc.getText().toString(),
etLocation.getText().toString(),
sdf.format(startCal.getTime()),
sdf.format(endCal.getTime()),
getCalendarSlug(),
getSelectedReminders(),
isAllDay,
selectedRRule
title, description, location, startTimeStr, endTimeStr,
getCalendarSlug(), reminders, isAllDay, selectedRRule
);
if (eventToEdit != null) {
@ -557,6 +534,10 @@ public class CreateEventFragment extends Fragment {
Toast.makeText(getContext(), eventToEdit != null ? "Oppdaterer..." : "Oppretter...", Toast.LENGTH_SHORT).show();
// **VIKTIG ENDRING:**
// Vi henter context her (mens Fragmentet lever) for å bruke den i bakgrunnstråden
final Context appContext = requireContext().getApplicationContext();
Call<JsonElement> call;
if (eventToEdit != null) {
call = RetrofitClient.getApiService().updateCalendarEvent(req);
@ -569,25 +550,38 @@ public class CreateEventFragment extends Fragment {
public void onResponse(Call<JsonElement> call, Response<JsonElement> response) {
if (response.isSuccessful()) {
Toast.makeText(getContext(), eventToEdit != null ? "Hendelse oppdatert!" : "Hendelse opprettet!", Toast.LENGTH_LONG).show();
// Start oppdatering av alarmer i bakgrunnen
fetchCalendarAndSchedule(appContext);
Navigation.findNavController(getView()).navigateUp();
} else {
String errorMsg = "Ukjent feil";
try {
if (response.errorBody() != null) {
errorMsg = response.errorBody().string();
}
} catch (Exception e) {}
Log.e("KBS_ERROR", "Server svarte med feil: " + errorMsg);
Toast.makeText(getContext(), "Feil (" + response.code() + "): Sjekk Logcat", Toast.LENGTH_LONG).show();
Toast.makeText(getContext(), "Feil (" + response.code() + ")", Toast.LENGTH_LONG).show();
}
}
@Override
public void onFailure(Call<JsonElement> call, Throwable t) {
Log.e("KBS_ERROR", "Nettverksfeil", t);
Toast.makeText(getContext(), "Nettverksfeil: " + t.getMessage(), Toast.LENGTH_SHORT).show();
Toast.makeText(getContext(), "Nettverksfeil", Toast.LENGTH_SHORT).show();
}
});
}
// Oppdatert metode som tar imot context som parameter
private void fetchCalendarAndSchedule(Context context) {
new Thread(() -> {
try {
// Sjekk en ekstra gang for å være sikker
if (context == null) return;
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
if (response.isSuccessful() && response.body() != null) {
// Bruk contexten vi fikk tilsendt, ikke getContext() som kan være null
AlarmScheduler.scheduleAlarmsForEvents(context, response.body());
}
} catch (Exception e) {
Log.e("CreateEvent", "Kunne ikke oppdatere alarmer", e);
}
}).start();
}
}

View file

@ -22,15 +22,12 @@ import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import java.text.SimpleDateFormat;
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;
@ -55,7 +52,7 @@ public class HomeFragment extends Fragment {
}
}
);
startNotificationWorker();
// GAMMEL METODE FJERNET HERFRA (startNotificationWorker)
}
@Nullable
@ -282,11 +279,4 @@ public class HomeFragment extends Fragment {
});
recyclerView.setAdapter(adapter);
}
private void startNotificationWorker() {
PeriodicWorkRequest notifRequest =
new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(requireContext()).enqueue(notifRequest);
}
}

View file

@ -1,6 +1,5 @@
package com.kbs.kbsintranett;
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.NotificationChannel;
@ -22,9 +21,6 @@ import androidx.core.content.ContextCompat;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
@ -32,8 +28,6 @@ import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com"; // Bytt med din egen hvis denne er feil
@ -60,10 +54,8 @@ public class MainActivity extends AppCompatActivity {
if (bottomNav != null) {
NavigationUI.setupWithNavController(bottomNav, navController);
// --- NYTT: Håndter "Reselection" (Klikk fanen man allerede er i) ---
// Håndter "Reselection" (Klikk fanen man allerede er i)
bottomNav.setOnItemReselectedListener(item -> {
// Dette fjerner alt som ligger "oppå" hovedsiden i stabelen.
// F.eks: Hjem -> Kalender Full -> (Klikk Hjem) -> Hjem
navController.popBackStack(item.getItemId(), false);
});
}
@ -94,7 +86,7 @@ public class MainActivity extends AppCompatActivity {
checkNotificationPermission();
checkExactAlarmPermission();
scheduleCalendarWork();
// GAMMEL METODE FJERNET HERFRA (NotificationWorker)
// --- 3. AUTENTISERING ---
checkLoginState();
@ -174,8 +166,8 @@ public class MainActivity extends AppCompatActivity {
private void checkNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
}
}
}
@ -197,15 +189,4 @@ public class MainActivity extends AppCompatActivity {
}
}
}
private void scheduleCalendarWork() {
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"KbsCalendarWork",
ExistingPeriodicWorkPolicy.UPDATE,
workRequest
);
}
}

View file

@ -15,51 +15,47 @@ import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "FCMService";
private static final String CHANNEL_ID = "kbs_calendar_channel"; // Samme kanal som før
private static final String CHANNEL_ID = "kbs_calendar_channel";
/**
* Kalles når en ny token genereres (f.eks. ved ny installasjon).
* Denne tokenen sendes til WordPress-backend slik at serveren vet hvem den skal sende til.
*/
@Override
public void onNewToken(@NonNull String token) {
super.onNewToken(token);
Log.d(TAG, "Ny FCM Token: " + token);
// TODO: Send denne tokenen til din WordPress-backend via AuthRepository eller RetrofitClient.
// F.eks: AuthRepository.updateDeviceToken(token);
// Vi lagrer den midlertidig i UserManager eller SharedPreferences hvis brukeren ikke er logget inn enda.
AuthRepository.updateDeviceToken(token);
}
/**
* Kalles når en melding mottas mens appen er i forgrunnen,
* ELLER hvis det er en "Data Message" (som er det vi bør bruke for bakgrunnsoppdateringer).
*/
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
Log.d(TAG, "Melding mottatt fra: " + remoteMessage.getFrom());
// Sjekk om meldingen inneholder data (payload)
// 1. Sjekk data payload (Bakgrunnsoppdatering)
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Melding data payload: " + remoteMessage.getData());
// Her kan du trigge en oppdatering av kalenderen i bakgrunnen uten å vise varsel,
// eller vise et varsel basert dataene.
String forceRefresh = remoteMessage.getData().get("force_refresh");
if ("true".equalsIgnoreCase(forceRefresh)) {
Log.d(TAG, "Mottok 'force_refresh' - oppdaterer kalender og alarmer...");
updateCalendarAndAlarms();
}
// Hvis meldingen også har egne titler i data-feltet (valgfritt)
String title = remoteMessage.getData().get("title");
String body = remoteMessage.getData().get("body");
if (title != null && body != null) {
showNotification(title, body);
}
}
// Sjekk om meldingen er en ren varslingsmelding (Notification payload)
// 2. Sjekk notification payload (Vises automatisk når app er i bakgrunn, men vi håndterer den her for forgrunn)
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Melding varsel body: " + remoteMessage.getNotification().getBody());
showNotification(
@ -69,11 +65,31 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
}
}
private void updateCalendarAndAlarms() {
// Vi bruker Retrofit for å hente kalenderen nytt
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
if (response.isSuccessful() && response.body() != null) {
// Lagre til cache først (god praksis)
CacheManager.saveCalendarEvents(getApplicationContext(), response.body());
// Oppdater alarmer lokalt
AlarmScheduler.scheduleAlarmsForEvents(getApplicationContext(), response.body());
Log.d(TAG, "Kalender og alarmer oppdatert via Push.");
}
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
Log.e(TAG, "Feil ved push-oppdatering av kalender", t);
}
});
}
private void showNotification(String title, String message) {
// Gjenbruk logikk for kanalopprettelse (sikkerhetsnett)
createNotificationChannel();
// Sjekk rettigheter for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return;
@ -99,7 +115,6 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
.setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// Bruk systemtid som ID for å unngå at varsler overskriver hverandre, eller en fast ID hvis ønskelig
notificationManager.notify((int) System.currentTimeMillis(), builder.build());
}

View file

@ -0,0 +1,16 @@
package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName;
public class RegisterDeviceRequest {
@SerializedName("fcm_token")
public String fcmToken;
@SerializedName("platform")
public String platform;
public RegisterDeviceRequest(String fcmToken) {
this.fcmToken = fcmToken;
this.platform = "android";
}
}

View file

@ -29,6 +29,9 @@ public class UserManager {
private String stilling;
private String mobiltelefon;
// FCM Token (Push)
private String fcmToken;
// NYTT:
private List<String> writeableCalendars = new ArrayList<>();
@ -79,6 +82,9 @@ public class UserManager {
public String getStilling() { return stilling != null ? stilling : ""; }
public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; }
public void setFcmToken(String token) { this.fcmToken = token; }
public String getFcmToken() { return fcmToken; }
public boolean isLoggedIn() { return userEmail != null && !userEmail.isEmpty(); }
public boolean isAdmin() { return "administrator".equalsIgnoreCase(userRole); }
public boolean isEditorOrAbove() {
@ -98,5 +104,6 @@ public class UserManager {
stilling = null;
mobiltelefon = null;
writeableCalendars.clear();
// Vi sletter ikke fcmToken ved logout, da enheten fortsatt er den samme
}
}

View file

@ -42,7 +42,6 @@ public interface WordPressApiService {
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
// DETTE ER METODEN SOM MANGLER:
@POST("wp-json/kbs/v1/calendar/create")
Call<JsonElement> createCalendarEvent(@Body CreateEventRequest request);
@ -72,7 +71,9 @@ public interface WordPressApiService {
Call<JsonElement> updateCalendarEvent(@Body CreateEventRequest request);
@POST("wp-json/kbs/v1/calendar/delete")
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request);
// NYTT: Registrer enhet for push-varsler
@POST("wp-json/kbs/v1/device/register")
Call<JsonElement> registerDevice(@Body RegisterDeviceRequest request);
}

View file

@ -58,8 +58,8 @@ android {
applicationId = "com.kbs.kbsintranett"
minSdk = 28
targetSdk = 34
versionCode = 3
versionName = "1.4"
versionCode = 4
versionName = "1.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -117,8 +117,10 @@ dependencies {
// 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
@ -229,6 +231,15 @@ FILSTI: app\src\main\AndroidManifest.xml
android:enabled="true"
android:exported="false" />
<!-- NYTT: Registrering av Firebase Messaging Service -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider"
@ -340,6 +351,7 @@ 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;
@ -395,6 +407,12 @@ public class AuthRepository {
// 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 {
@ -410,6 +428,35 @@ public class AuthRepository {
}
});
}
/**
* 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<JsonElement>() {
@Override
public void onResponse(Call<JsonElement> call, Response<JsonElement> 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<JsonElement> call, Throwable t) {
Log.e(TAG, "Nettverksfeil ved sending av FCM token", t);
}
});
}
}
============================================================
@ -5761,6 +5808,131 @@ public class MainActivity extends AppCompatActivity {
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\MyFirebaseMessagingService.java
============================================================
package com.kbs.kbsintranett;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "FCMService";
private static final String CHANNEL_ID = "kbs_calendar_channel"; // Samme kanal som før
/**
* Kalles når en ny token genereres (f.eks. ved ny installasjon).
* Denne tokenen MÅ sendes til WordPress-backend slik at serveren vet hvem den skal sende til.
*/
@Override
public void onNewToken(@NonNull String token) {
super.onNewToken(token);
Log.d(TAG, "Ny FCM Token: " + token);
// Oppdater token via AuthRepository.
// Hvis brukeren er logget inn, sendes den direkte.
// Hvis ikke, lagres den i UserManager og sendes ved neste login.
AuthRepository.updateDeviceToken(token);
}
/**
* Kalles når en melding mottas mens appen er i forgrunnen,
* ELLER hvis det er en "Data Message" (som er det vi bør bruke for bakgrunnsoppdateringer).
*/
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
super.onMessageReceived(remoteMessage);
Log.d(TAG, "Melding mottatt fra: " + remoteMessage.getFrom());
// Sjekk om meldingen inneholder data (payload)
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Melding data payload: " + remoteMessage.getData());
// Her kan du trigge en oppdatering av kalenderen i bakgrunnen uten å vise varsel,
// eller vise et varsel basert på dataene.
String title = remoteMessage.getData().get("title");
String body = remoteMessage.getData().get("body");
if (title != null && body != null) {
showNotification(title, body);
}
}
// Sjekk om meldingen er en ren varslingsmelding (Notification payload)
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Melding varsel body: " + remoteMessage.getNotification().getBody());
showNotification(
remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody()
);
}
}
private void showNotification(String title, String message) {
// Gjenbruk logikk for kanalopprettelse (sikkerhetsnett)
createNotificationChannel();
// Sjekk rettigheter for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return;
}
}
Intent tapIntent = new Intent(this, MainActivity.class);
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
tapIntent,
PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_kbs)
.setColor(ContextCompat.getColor(this, R.color.kbs_logo_blue))
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
// Bruk systemtid som ID for å unngå at varsler overskriver hverandre, eller en fast ID hvis ønskelig
notificationManager.notify((int) System.currentTimeMillis(), builder.build());
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"KBS Kalendervarsler",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Varsler fra KBS Intranett");
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\NewsAdapter.java
============================================================
@ -6405,6 +6577,26 @@ public class ProfileFragment extends Fragment {
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\RegisterDeviceRequest.java
============================================================
package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName;
public class RegisterDeviceRequest {
@SerializedName("fcm_token")
public String fcmToken;
@SerializedName("platform")
public String platform;
public RegisterDeviceRequest(String fcmToken) {
this.fcmToken = fcmToken;
this.platform = "android";
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\RetrofitClient.java
============================================================
@ -6511,6 +6703,9 @@ public class UserManager {
private String stilling;
private String mobiltelefon;
// FCM Token (Push)
private String fcmToken;
// NYTT:
private List<String> writeableCalendars = new ArrayList<>();
@ -6561,6 +6756,9 @@ public class UserManager {
public String getStilling() { return stilling != null ? stilling : ""; }
public String getMobiltelefon() { return mobiltelefon != null ? mobiltelefon : ""; }
public void setFcmToken(String token) { this.fcmToken = token; }
public String getFcmToken() { return fcmToken; }
public boolean isLoggedIn() { return userEmail != null && !userEmail.isEmpty(); }
public boolean isAdmin() { return "administrator".equalsIgnoreCase(userRole); }
public boolean isEditorOrAbove() {
@ -6580,6 +6778,7 @@ public class UserManager {
stilling = null;
mobiltelefon = null;
writeableCalendars.clear();
// Vi sletter ikke fcmToken ved logout, da enheten fortsatt er den samme
}
}
@ -6688,7 +6887,6 @@ public interface WordPressApiService {
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
// DETTE ER METODEN SOM MANGLER:
@POST("wp-json/kbs/v1/calendar/create")
Call<JsonElement> createCalendarEvent(@Body CreateEventRequest request);
@ -6718,9 +6916,11 @@ public interface WordPressApiService {
Call<JsonElement> updateCalendarEvent(@Body CreateEventRequest request);
@POST("wp-json/kbs/v1/calendar/delete")
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type
Call<JsonElement> deleteCalendarEvent(@Body CreateEventRequest request);
// NYTT: Registrer enhet for push-varsler
@POST("wp-json/kbs/v1/device/register")
Call<JsonElement> registerDevice(@Body RegisterDeviceRequest request);
}
============================================================