diff --git a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java b/app/src/main/java/com/kbs/kbsintranett/AlarmScheduler.java similarity index 53% rename from app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java rename to app/src/main/java/com/kbs/kbsintranett/AlarmScheduler.java index 72fc83b..de09ee5 100644 --- a/app/src/main/java/com/kbs/kbsintranett/NotificationWorker.java +++ b/app/src/main/java/com/kbs/kbsintranett/AlarmScheduler.java @@ -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> 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 events) { - Context context = getApplicationContext(); + /** + * 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); - - 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 nå "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 - if (prefs.getBoolean(alarmKey, false)) { - continue; // Allerede håndtert - } + // Hvis tidspunktet er i fremtiden (og innenfor vinduet) + if (triggerTime > now && triggerTime < futureWindow) { - if (triggerTime > catchUpWindow && triggerTime < futureWindow) { - if (triggerTime < now) { - triggerTime = now + 1000; // Catch-up + // Sjekk om vi allerede har satt denne alarmen for å unngå dobbeltarbeid + if (prefs.getBoolean(alarmKey, false)) { + continue; } - int alarmId = alarmKey.hashCode(); + 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); @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java index 364d870..d42ecb3 100644 --- a/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java +++ b/app/src/main/java/com/kbs/kbsintranett/AuthRepository.java @@ -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 nå 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() { + @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); + } + }); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java index 9ef3b46..159899c 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java +++ b/app/src/main/java/com/kbs/kbsintranett/CalendarEvent.java @@ -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 på en gang + public void setRemindersList(List reminders) { + this.reminders = reminders != null ? new ArrayList<>(reminders) : new ArrayList<>(); + } + public int getReminderMinutes() { if (reminders != null && !reminders.isEmpty()) { return reminders.get(0); diff --git a/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java b/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java index 07500a1..398ec55 100644 --- a/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/CreateEventFragment.java @@ -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 adapter = (ArrayAdapter) 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 calendars = UserManager.getInstance().getWriteableCalendars(); if (calendars.isEmpty()) calendars.add("Felles"); - // Vi bruker en Custom Adapter for å styre farger ArrayAdapter adapter = new ArrayAdapter(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 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 call; if (eventToEdit != null) { call = RetrofitClient.getApiService().updateCalendarEvent(req); @@ -569,25 +550,38 @@ public class CreateEventFragment extends Fragment { public void onResponse(Call call, Response 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 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> 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(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java index a4bdbee..536794b 100644 --- a/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java +++ b/app/src/main/java/com/kbs/kbsintranett/HomeFragment.java @@ -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); - } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java index 952da2c..7b9e0b5 100644 --- a/app/src/main/java/com/kbs/kbsintranett/MainActivity.java +++ b/app/src/main/java/com/kbs/kbsintranett/MainActivity.java @@ -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 på fanen man allerede er i) --- + // Håndter "Reselection" (Klikk på 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 - ); - } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java b/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java index 4bfdfe8..b928d10 100644 --- a/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java +++ b/app/src/main/java/com/kbs/kbsintranett/MyFirebaseMessagingService.java @@ -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 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); - - // 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 på 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 på nytt + RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> 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> 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()); } diff --git a/app/src/main/java/com/kbs/kbsintranett/RegisterDeviceRequest.java b/app/src/main/java/com/kbs/kbsintranett/RegisterDeviceRequest.java new file mode 100644 index 0000000..aec804a --- /dev/null +++ b/app/src/main/java/com/kbs/kbsintranett/RegisterDeviceRequest.java @@ -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"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/UserManager.java b/app/src/main/java/com/kbs/kbsintranett/UserManager.java index 3b91c97..e3253ca 100644 --- a/app/src/main/java/com/kbs/kbsintranett/UserManager.java +++ b/app/src/main/java/com/kbs/kbsintranett/UserManager.java @@ -29,6 +29,9 @@ public class UserManager { private String stilling; private String mobiltelefon; + // FCM Token (Push) + private String fcmToken; + // NYTT: private List 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java index 983eedd..726539a 100644 --- a/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java +++ b/app/src/main/java/com/kbs/kbsintranett/WordPressApiService.java @@ -42,7 +42,6 @@ public interface WordPressApiService { @GET("wp-json/kbs/v1/calendar/events") Call> getCalendarEvents(); - // DETTE ER METODEN SOM MANGLER: @POST("wp-json/kbs/v1/calendar/create") Call createCalendarEvent(@Body CreateEventRequest request); @@ -72,7 +71,9 @@ public interface WordPressApiService { Call updateCalendarEvent(@Body CreateEventRequest request); @POST("wp-json/kbs/v1/calendar/delete") - Call deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type - + Call deleteCalendarEvent(@Body CreateEventRequest request); + // NYTT: Registrer enhet for push-varsler + @POST("wp-json/kbs/v1/device/register") + Call registerDevice(@Body RegisterDeviceRequest request); } \ No newline at end of file diff --git a/hele_prosjektet.txt b/hele_prosjektet.txt index 09481e9..91f67b8 100644 --- a/hele_prosjektet.txt +++ b/hele_prosjektet.txt @@ -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" } @@ -111,14 +111,16 @@ dependencies { // 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 @@ -229,6 +231,15 @@ FILSTI: app\src\main\AndroidManifest.xml android:enabled="true" android:exported="false" /> + + + + + + + () { + @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); + } + }); + } } ============================================================ @@ -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 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> getCalendarEvents(); - // DETTE ER METODEN SOM MANGLER: @POST("wp-json/kbs/v1/calendar/create") Call createCalendarEvent(@Body CreateEventRequest request); @@ -6718,9 +6916,11 @@ public interface WordPressApiService { Call updateCalendarEvent(@Body CreateEventRequest request); @POST("wp-json/kbs/v1/calendar/delete") - Call deleteCalendarEvent(@Body CreateEventRequest request); // Sender kun ID og cal_type - + Call deleteCalendarEvent(@Body CreateEventRequest request); + // NYTT: Registrer enhet for push-varsler + @POST("wp-json/kbs/v1/device/register") + Call registerDevice(@Body RegisterDeviceRequest request); } ============================================================