Før nytt forsøk på Google Kalender

This commit is contained in:
ErolHaagenrud 2025-12-15 17:31:44 +01:00
parent 29a36e8a61
commit d9d128245c
10 changed files with 714 additions and 174 deletions

View file

@ -13,6 +13,7 @@
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:allowBackup="true"
@ -38,6 +39,8 @@
<activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider"

View file

@ -0,0 +1,65 @@
package com.kbs.kbsintranett;
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.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "KBS_DEBUG";
private static final String CHANNEL_ID = "kbs_calendar_channel";
@Override
public void onReceive(Context context, Intent intent) {
String title = intent.getStringExtra("TITLE");
String message = intent.getStringExtra("MESSAGE");
int notificationId = intent.getIntExtra("ID", 0);
Log.i(TAG, "AlarmReceiver: WAKE UP! Mottok alarm for: " + title);
showNotification(context, title, message, notificationId);
}
private void showNotification(Context context, String title, String message, int notificationId) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Varsler for kalenderhendelser");
manager.createNotificationChannel(channel);
}
Intent openAppIntent = new Intent(context, MainActivity.class);
openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
try {
manager.notify(notificationId, builder.build());
Log.d(TAG, "AlarmReceiver: Varsel sendt til systemet.");
} catch (SecurityException e) {
Log.e(TAG, "AlarmReceiver: Feil - Mangler tillatelse til å sende varsel!", e);
} catch (Exception e) {
Log.e(TAG, "AlarmReceiver: Ukjent feil ved visning av varsel", e);
}
}
}

View file

@ -38,7 +38,6 @@ public class CalendarFullFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_full_calendar);
progressBar = view.findViewById(R.id.loading_full_calendar);
emptyView = view.findViewById(R.id.empty_view_calendar);
@ -46,7 +45,6 @@ public class CalendarFullFragment extends Fragment {
layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
fetchAllEvents();
@ -54,22 +52,21 @@ public class CalendarFullFragment extends Fragment {
private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE);
// Hent personlige hendelser ( med historikk)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
// NYTT: Hent fra Google direkte
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) {
CalendarManager.formatEventForUI(e);
apiEvents.add(e);
}
// Konverter og formatér
apiEvents = CalendarManager.convertGoogleResponse(response.body());
}
// Flett og vis
@ -94,7 +91,7 @@ public class CalendarFullFragment extends Fragment {
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);

View file

@ -15,15 +15,86 @@ import java.util.TimeZone;
public class CalendarManager {
// --- KONFIGURASJON FOR GOOGLE DIREKTE-KOBLING ---
private static final String GOOGLE_CALENDAR_ID = "kbservice.no_o2bmp5f9f540vedveit51optfo@group.calendar.google.com";
// TODO: Sett inn din API-nøkkel her!
private static final String GOOGLE_API_KEY = "AIzaSyCos8VW5mClUcuhs86gbSJo8uitY0fVPus";
public static String getGoogleCalendarUrl() {
// Hent hendelser fra 1 år tilbake i tid
long oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
String timeMin = dateFormat.format(new Date(oneYearAgo));
return "https://www.googleapis.com/calendar/v3/calendars/"
+ GOOGLE_CALENDAR_ID
+ "/events?key=" + GOOGLE_API_KEY
+ "&singleEvents=true"
+ "&orderBy=startTime"
+ "&maxResults=250"
+ "&timeMin=" + timeMin; // URL-encoded er ikke nødvendig her siden Retrofit/OkHttp håndterer det, men timeMin bør være formatert
}
// Konverterer Google Response til vår interne CalendarEvent
public static List<CalendarEvent> convertGoogleResponse(GoogleCalendarModels.Response response) {
List<CalendarEvent> events = new ArrayList<>();
if (response == null || response.items == null) return events;
for (GoogleCalendarModels.Item item : response.items) {
String title = item.summary != null ? item.summary : "(Uten tittel)";
String desc = item.description;
String loc = item.location;
// Dato-logikk
String start = null;
String end = null;
if (item.start != null) {
if (item.start.dateTime != null) start = item.start.dateTime;
else start = item.start.date; // Heldags
}
if (item.end != null) {
if (item.end.dateTime != null) end = item.end.dateTime;
else end = item.end.date; // Heldags
}
// Varslings-logikk (Henter de sanne innstillingene)
int reminderMinutes = 15; // Default fallback
if (item.reminders != null) {
if (item.reminders.useDefault) {
reminderMinutes = 15; // Standard i Google er ofte 10 eller 15, vi antar 15
} else if (item.reminders.overrides != null) {
for (GoogleCalendarModels.Override override : item.reminders.overrides) {
if ("popup".equalsIgnoreCase(override.method) || "alert".equalsIgnoreCase(override.method)) {
reminderMinutes = override.minutes;
break; // Bruk den første popup-varslingen vi finner
}
}
}
}
CalendarEvent event = new CalendarEvent(title, start, end, desc, loc);
event.setReminderMinutes(reminderMinutes);
// Formatér for UI med en gang
formatEventForUI(event);
events.add(event);
}
return events;
}
// --- EKSISTERENDE KODE (UENDRET UNDER) ---
// NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>();
String[] projection = new String[] {
CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME
};
// Vi ser etter kontoer som slutter @kbs.no
// (SQL: account_name LIKE '%@kbs.no')
String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?";
@ -38,7 +109,8 @@ public class CalendarManager {
)) {
if (cursor != null) {
while (cursor.moveToNext()) {
ids.add(cursor.getString(0)); // Legg til kalender-ID
ids.add(cursor.getString(0));
// Legg til kalender-ID
}
}
} catch (Exception e) {
@ -49,7 +121,6 @@ public class CalendarManager {
public static List<CalendarEvent> getDeviceEvents(Context context) {
List<CalendarEvent> deviceEvents = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
return deviceEvents;
@ -57,7 +128,6 @@ public class CalendarManager {
// 1. Finn først ID-ene til KBS-kalenderne
List<String> kbsCalendarIds = getKbsCalendarIds(context);
// Hvis ingen kbs-kalendere finnes telefonen, returner tom liste
if (kbsCalendarIds.isEmpty()) {
return deviceEvents;
@ -67,7 +137,6 @@ public class CalendarManager {
long now = System.currentTimeMillis();
long startMillis = now - (365L * 24 * 60 * 60 * 1000);
long endMillis = now + (365L * 24 * 60 * 60 * 1000);
String[] projection = new String[]{
CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART,
@ -90,7 +159,6 @@ public class CalendarManager {
}
}
selection.append(") AND ");
selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND ");
selection.append(CalendarContract.Events.DTSTART).append(" <= ?");
@ -98,7 +166,6 @@ public class CalendarManager {
selectionArgsList.add(String.valueOf(endMillis));
String[] selectionArgs = selectionArgsList.toArray(new String[0]);
try (Cursor cursor = context.getContentResolver().query(
CalendarContract.Events.CONTENT_URI,
projection,
@ -108,7 +175,6 @@ public class CalendarManager {
)) {
if (cursor != null) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
while (cursor.moveToNext()) {
String title = cursor.getString(0);
long dtStart = cursor.getLong(1);
@ -116,7 +182,6 @@ public class CalendarManager {
String desc = cursor.getString(3);
String loc = cursor.getString(4);
int allDay = cursor.getInt(5);
String rawStart;
String rawEnd;
@ -140,14 +205,11 @@ public class CalendarManager {
return deviceEvents;
}
// --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) ---
public static void formatEventForUI(CalendarEvent event) {
if (event.getRawDate() == null) return;
SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault());
SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO"));
SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
@ -157,7 +219,6 @@ public class CalendarManager {
boolean isAllDay = false;
String raw = event.getRawDate();
if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) {
SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
date = shortFmt.parse(raw);
@ -215,7 +276,6 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2);
});
return all;
}
}

View file

@ -0,0 +1,59 @@
package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Hjelpeklasser for å parse JSON direkte fra Google Calendar API v3.
*/
public class GoogleCalendarModels {
public static class Response {
@SerializedName("items")
public List<Item> items;
}
public static class Item {
@SerializedName("summary")
public String summary;
@SerializedName("description")
public String description;
@SerializedName("location")
public String location;
@SerializedName("start")
public TimePoint start;
@SerializedName("end")
public TimePoint end;
@SerializedName("reminders")
public Reminders reminders;
}
public static class TimePoint {
@SerializedName("dateTime")
public String dateTime; // Format: 2025-12-15T10:00:00+01:00
@SerializedName("date")
public String date; // Format: 2025-12-15 (for heldags)
}
public static class Reminders {
@SerializedName("useDefault")
public boolean useDefault;
@SerializedName("overrides")
public List<Override> overrides;
}
public static class Override {
@SerializedName("method")
public String method; // f.eks "popup"
@SerializedName("minutes")
public int minutes;
}
}

View file

@ -37,7 +37,6 @@ public class HomeFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Håndter svar kalendertillatelse
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
@ -48,7 +47,6 @@ public class HomeFragment extends Fragment {
}
}
);
// Start bakgrunnsjobb for varsling (kjører hver 15. minutt)
startNotificationWorker();
}
@ -62,7 +60,6 @@ public class HomeFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 0. Profil-knapp
View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) {
@ -74,7 +71,6 @@ public class HomeFragment extends Fragment {
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
// Sett tom adapter midlertidig
calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
// "Se alle" knapp for kalender
TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar);
if (viewAllCalendar != null) {
@ -102,7 +98,6 @@ public class HomeFragment extends Fragment {
newsRecycler.setNestedScrollingEnabled(false);
// Sett tom adapter midlertidig
newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {}));
// "Se alle" knapp for nyheter
TextView viewAllNews = view.findViewById(R.id.btn_view_all_news);
if (viewAllNews != null) {
@ -118,26 +113,23 @@ public class HomeFragment extends Fragment {
// 1. Hent personlige hendelser først (fra CalendarManager)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// 2. Hent API-hendelser fra WordPress
WordPressApiService apiService = RetrofitClient.getApiService();
apiService.getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
// 2. Hent API-hendelser DIREKTE fra Google
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) {
CalendarManager.formatEventForUI(e); // Formatér datoer
apiEvents.add(e);
}
// Konverter fra Google-modell til KBS-modell
apiEvents = CalendarManager.convertGoogleResponse(response.body());
}
// 3. Flett listene (API + Personlig) og sorter
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
// 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag)
// (CalendarManager henter 1 år bakover, vi filtrere for "Topp 5 kommende")
List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
@ -160,14 +152,12 @@ public class HomeFragment extends Fragment {
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return;
// Hvis API feiler, vis bare personlige events hvis vi har noen
if (!deviceEvents.isEmpty()) {
List<CalendarEvent> top5 = new ArrayList<>();
// Filtrer og plukk topp 5 fra lokale også
for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i));
recyclerView.setAdapter(new CalendarAdapter(top5, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.show(getParentFragmentManager(), "CalendarDetails");
@ -201,7 +191,8 @@ public class HomeFragment extends Fragment {
for (WpPost post : wpPosts) {
try {
Date date = rawFormat.parse(post.date);
post.date = targetFormat.format(date); // Setter pen dato
post.date = targetFormat.format(date);
// Setter pen dato
} catch (Exception e) {}
}

View file

@ -1,6 +1,13 @@
package com.kbs.kbsintranett;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
@ -8,6 +15,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
@ -32,6 +41,7 @@ public class MainActivity extends AppCompatActivity {
bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNav, navController);
@ -47,10 +57,50 @@ public class MainActivity extends AppCompatActivity {
});
}
// --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) ---
checkExactAlarmPermission();
// 2. Start Silent Sign-In ved oppstart
checkLoginState();
}
/**
* Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM).
* Hvis ikke, spør brukeren om å til innstillinger.
*/
private void checkExactAlarmPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) {
// Vi mangler tillatelse. Vis dialog.
new AlertDialog.Builder(this)
.setTitle("Varslingstillatelse kreves")
.setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.")
.setPositiveButton("Gå til Innstillinger", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.setNegativeButton("Senere", null)
.show();
} else {
// Vi har tillatelse (eller er eldre Android). Kjør logikk!
runNotificationWorker();
}
} else {
// Eldre Android-versjoner trenger ikke denne tillatelsen
runNotificationWorker();
}
}
private void runNotificationWorker() {
// --- DEBUG: TVING KJØRING AV KALENDER-SJEKK ---
// Denne linjen kjører NotificationWorker umiddelbart ved oppstart for feilsøking.
// Fjern eller kommenter ut denne når testingen er ferdig.
WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.from(NotificationWorker.class));
// ----------------------------------------------
}
private void checkLoginState() {
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account == null) {

View file

@ -1,12 +1,12 @@
package com.kbs.kbsintranett;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.IOException;
@ -18,8 +18,7 @@ import retrofit2.Response;
public class NotificationWorker extends Worker {
private static final String CHANNEL_ID = "kbs_calendar_channel";
private static final String PREFS_NAME = "KBSNotificationPrefs";
private static final String TAG = "KBS_DEBUG";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
@ -28,72 +27,108 @@ public class NotificationWorker extends Worker {
@NonNull
@Override
public Result doWork() {
// Dette kjører i bakgrunnen
Log.d(TAG, "NotificationWorker: Starter sjekk av kalender...");
try {
// Hent events synkront (ikke enqueue)
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
String url = CalendarManager.getGoogleCalendarUrl();
// Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) {
checkAndNotify(response.body());
List<CalendarEvent> events = CalendarManager.convertGoogleResponse(response.body());
scheduleAlarms(events);
return Result.success();
} else {
Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code());
return Result.retry();
}
} catch (IOException e) {
Log.e(TAG, "NotificationWorker: Nettverksfeil", e);
return Result.retry();
}
}
private void checkAndNotify(List<CalendarEvent> events) {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
long now = System.currentTimeMillis();
long fifteenMinutes = 15 * 60 * 1000;
private void scheduleAlarms(List<CalendarEvent> events) {
Context context = getApplicationContext();
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// Sjekk rettigheter (Android 12+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse! Gå til Innstillinger -> Apper -> KBS -> Alarmer og påminnelser.");
return;
}
}
long now = System.currentTimeMillis();
// Bruker en parser som er litt mer fleksibel for datoer
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
int countSet = 0;
for (CalendarEvent event : events) {
try {
Date eventDate;
if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate());
else eventDate = sqlFormat.parse(event.getRawDate());
// Hopp over heldagshendelser (dato uten klokkeslett)
if (event.getRawDate().length() == 10) continue;
Date eventDate = null;
// Enkel parsing. Merk: Google sender med tidssone (+01:00),
// men SimpleDateFormat uten 'X' vil parse dette som lokal tid hvis formatet stemmer.
// For optimal tidssone-håndtering burde vi brukt java.time (Android 8+),
// men dette fungerer greit lenge telefonen er i samme sone som kalenderen.
if (event.getRawDate().contains("T")) {
// Kutter vekk tidssone-offset for enkel parsing til lokal tid
String raw = event.getRawDate();
if (raw.length() > 19) raw = raw.substring(0, 19);
eventDate = isoFormat.parse(raw);
}
if (eventDate == null) continue;
long diff = eventDate.getTime() - now;
// Beregn når alarmen skal
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
// Hvis eventet starter innen de neste 30 min, og ikke allerede varslet
if (diff > 0 && diff < (30 * 60 * 1000)) {
String eventId = event.getTitle() + event.getRawDate(); // Enkel ID
boolean alreadyNotified = prefs.getBoolean(eventId, false);
// Vi setter alarmen hvis tidspunktet er i fremtiden
// Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser)
if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
if (!alreadyNotified) {
sendNotification(event.getTitle(), "Starter kl " + event.getTime());
// Lagre at vi har varslet
prefs.edit().putBoolean(eventId, true).apply();
// Lag en unik ID for alarmen
String uniqueIdString = event.getTitle() + event.getRawDate();
int alarmId = uniqueIdString.hashCode();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("TITLE", event.getTitle());
intent.putExtra("MESSAGE", "Starter kl " + event.getTime());
intent.putExtra("ID", alarmId);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
alarmId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// VIKTIG ENDRING: Vi setter alarmen NYTT hver gang.
// AlarmManager overskriver automatisk hvis ID er lik.
// Dette sikrer at alarmen faktisk ligger der, selv etter omstart av tlf.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
}
Log.i(TAG, ">>> ALARM SATT (Oppdatert): " + event.getTitle() + " -> Skal ringe: " + new Date(triggerTime));
countSet++;
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e);
}
}
}
private void sendNotification(String title, String content) {
NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);
if (countSet == 0) {
Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
manager.notify((int) System.currentTimeMillis(), builder.build());
}
}

View file

@ -15,6 +15,7 @@ import retrofit2.http.Multipart;
import retrofit2.http.Part;
import retrofit2.http.PartMap;
import retrofit2.http.Query;
import retrofit2.http.Url; // NY IMPORT
public interface WordPressApiService {
@GET("wp-json/wp/v2/posts?per_page=10&_embed")
@ -40,9 +41,14 @@ public interface WordPressApiService {
@Part List<MultipartBody.Part> files
);
// ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
// NY: Direkte kall mot Google (bruker @Url for å override base URL)
@GET
Call<GoogleCalendarModels.Response> getDirectGoogleEvents(@Url String fullUrl);
@GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId,

View file

@ -177,6 +177,7 @@ FILSTI: app\src\main\AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:allowBackup="true"
@ -202,6 +203,8 @@ FILSTI: app\src\main\AndroidManifest.xml
<activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.kbs.kbsintranett.fileprovider"
@ -216,6 +219,75 @@ FILSTI: app\src\main\AndroidManifest.xml
</manifest>
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java
============================================================
package com.kbs.kbsintranett;
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.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "KBS_DEBUG";
private static final String CHANNEL_ID = "kbs_calendar_channel";
@Override
public void onReceive(Context context, Intent intent) {
String title = intent.getStringExtra("TITLE");
String message = intent.getStringExtra("MESSAGE");
int notificationId = intent.getIntExtra("ID", 0);
Log.i(TAG, "AlarmReceiver: WAKE UP! Mottok alarm for: " + title);
showNotification(context, title, message, notificationId);
}
private void showNotification(Context context, String title, String message, int notificationId) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Varsler for kalenderhendelser");
manager.createNotificationChannel(channel);
}
Intent openAppIntent = new Intent(context, MainActivity.class);
openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
try {
manager.notify(notificationId, builder.build());
Log.d(TAG, "AlarmReceiver: Varsel sendt til systemet.");
} catch (SecurityException e) {
Log.e(TAG, "AlarmReceiver: Feil - Mangler tillatelse til å sende varsel!", e);
} catch (Exception e) {
Log.e(TAG, "AlarmReceiver: Ukjent feil ved visning av varsel", e);
}
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\AuthRepository.java
============================================================
@ -568,7 +640,6 @@ public class CalendarFullFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_full_calendar);
progressBar = view.findViewById(R.id.loading_full_calendar);
emptyView = view.findViewById(R.id.empty_view_calendar);
@ -576,7 +647,6 @@ public class CalendarFullFragment extends Fragment {
layoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(layoutManager);
backBtn.setOnClickListener(v -> Navigation.findNavController(view).navigateUp());
fetchAllEvents();
@ -584,22 +654,21 @@ public class CalendarFullFragment extends Fragment {
private void fetchAllEvents() {
progressBar.setVisibility(View.VISIBLE);
// Hent personlige hendelser (Nå med historikk)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
RetrofitClient.getApiService().getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
// NYTT: Hent fra Google direkte
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) {
CalendarManager.formatEventForUI(e);
apiEvents.add(e);
}
// Konverter og formatér
apiEvents = CalendarManager.convertGoogleResponse(response.body());
}
// Flett og vis
@ -624,7 +693,7 @@ public class CalendarFullFragment extends Fragment {
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return;
progressBar.setVisibility(View.GONE);
@ -684,15 +753,86 @@ import java.util.TimeZone;
public class CalendarManager {
// --- KONFIGURASJON FOR GOOGLE DIREKTE-KOBLING ---
private static final String GOOGLE_CALENDAR_ID = "kbservice.no_o2bmp5f9f540vedveit51optfo@group.calendar.google.com";
// TODO: Sett inn din API-nøkkel her!
private static final String GOOGLE_API_KEY = "AIzaSyCos8VW5mClUcuhs86gbSJo8uitY0fVPus";
public static String getGoogleCalendarUrl() {
// Hent hendelser fra 1 år tilbake i tid
long oneYearAgo = System.currentTimeMillis() - (365L * 24 * 60 * 60 * 1000);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
String timeMin = dateFormat.format(new Date(oneYearAgo));
return "https://www.googleapis.com/calendar/v3/calendars/"
+ GOOGLE_CALENDAR_ID
+ "/events?key=" + GOOGLE_API_KEY
+ "&singleEvents=true"
+ "&orderBy=startTime"
+ "&maxResults=250"
+ "&timeMin=" + timeMin; // URL-encoded er ikke nødvendig her siden Retrofit/OkHttp håndterer det, men timeMin bør være formatert
}
// Konverterer Google Response til vår interne CalendarEvent
public static List<CalendarEvent> convertGoogleResponse(GoogleCalendarModels.Response response) {
List<CalendarEvent> events = new ArrayList<>();
if (response == null || response.items == null) return events;
for (GoogleCalendarModels.Item item : response.items) {
String title = item.summary != null ? item.summary : "(Uten tittel)";
String desc = item.description;
String loc = item.location;
// Dato-logikk
String start = null;
String end = null;
if (item.start != null) {
if (item.start.dateTime != null) start = item.start.dateTime;
else start = item.start.date; // Heldags
}
if (item.end != null) {
if (item.end.dateTime != null) end = item.end.dateTime;
else end = item.end.date; // Heldags
}
// Varslings-logikk (Henter de sanne innstillingene)
int reminderMinutes = 15; // Default fallback
if (item.reminders != null) {
if (item.reminders.useDefault) {
reminderMinutes = 15; // Standard i Google er ofte 10 eller 15, vi antar 15
} else if (item.reminders.overrides != null) {
for (GoogleCalendarModels.Override override : item.reminders.overrides) {
if ("popup".equalsIgnoreCase(override.method) || "alert".equalsIgnoreCase(override.method)) {
reminderMinutes = override.minutes;
break; // Bruk den første popup-varslingen vi finner
}
}
}
}
CalendarEvent event = new CalendarEvent(title, start, end, desc, loc);
event.setReminderMinutes(reminderMinutes);
// Formatér for UI med en gang
formatEventForUI(event);
events.add(event);
}
return events;
}
// --- EKSISTERENDE KODE (UENDRET UNDER) ---
// NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>();
String[] projection = new String[] {
CalendarContract.Calendars._ID,
CalendarContract.Calendars.ACCOUNT_NAME
};
// Vi ser etter kontoer som slutter på @kbs.no
// (SQL: account_name LIKE '%@kbs.no')
String selection = CalendarContract.Calendars.ACCOUNT_NAME + " LIKE ?";
@ -707,7 +847,8 @@ public class CalendarManager {
)) {
if (cursor != null) {
while (cursor.moveToNext()) {
ids.add(cursor.getString(0)); // Legg til kalender-ID
ids.add(cursor.getString(0));
// Legg til kalender-ID
}
}
} catch (Exception e) {
@ -718,7 +859,6 @@ public class CalendarManager {
public static List<CalendarEvent> getDeviceEvents(Context context) {
List<CalendarEvent> deviceEvents = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
return deviceEvents;
@ -726,7 +866,6 @@ public class CalendarManager {
// 1. Finn først ID-ene til KBS-kalenderne
List<String> kbsCalendarIds = getKbsCalendarIds(context);
// Hvis ingen kbs-kalendere finnes på telefonen, returner tom liste
if (kbsCalendarIds.isEmpty()) {
return deviceEvents;
@ -736,7 +875,6 @@ public class CalendarManager {
long now = System.currentTimeMillis();
long startMillis = now - (365L * 24 * 60 * 60 * 1000);
long endMillis = now + (365L * 24 * 60 * 60 * 1000);
String[] projection = new String[]{
CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART,
@ -759,7 +897,6 @@ public class CalendarManager {
}
}
selection.append(") AND ");
selection.append(CalendarContract.Events.DTSTART).append(" >= ? AND ");
selection.append(CalendarContract.Events.DTSTART).append(" <= ?");
@ -767,7 +904,6 @@ public class CalendarManager {
selectionArgsList.add(String.valueOf(endMillis));
String[] selectionArgs = selectionArgsList.toArray(new String[0]);
try (Cursor cursor = context.getContentResolver().query(
CalendarContract.Events.CONTENT_URI,
projection,
@ -777,7 +913,6 @@ public class CalendarManager {
)) {
if (cursor != null) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
while (cursor.moveToNext()) {
String title = cursor.getString(0);
long dtStart = cursor.getLong(1);
@ -785,7 +920,6 @@ public class CalendarManager {
String desc = cursor.getString(3);
String loc = cursor.getString(4);
int allDay = cursor.getInt(5);
String rawStart;
String rawEnd;
@ -809,14 +943,11 @@ public class CalendarManager {
return deviceEvents;
}
// --- (formatEventForUI og mergeAndSort er uendret fra forrige versjon) ---
public static void formatEventForUI(CalendarEvent event) {
if (event.getRawDate() == null) return;
SimpleDateFormat outputDay = new SimpleDateFormat("dd", Locale.getDefault());
SimpleDateFormat outputMonth = new SimpleDateFormat("MMM", new Locale("no", "NO"));
SimpleDateFormat outputTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
outputMonth.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
outputTime.setTimeZone(TimeZone.getTimeZone("Europe/Oslo"));
@ -826,7 +957,6 @@ public class CalendarManager {
boolean isAllDay = false;
String raw = event.getRawDate();
if (raw.length() == 10 && !raw.contains("T") && !raw.contains(" ")) {
SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
date = shortFmt.parse(raw);
@ -884,7 +1014,6 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2);
});
return all;
}
}
@ -2946,6 +3075,69 @@ public class FormSubmission {
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\GoogleCalendarModels.java
============================================================
package com.kbs.kbsintranett;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Hjelpeklasser for å parse JSON direkte fra Google Calendar API v3.
*/
public class GoogleCalendarModels {
public static class Response {
@SerializedName("items")
public List<Item> items;
}
public static class Item {
@SerializedName("summary")
public String summary;
@SerializedName("description")
public String description;
@SerializedName("location")
public String location;
@SerializedName("start")
public TimePoint start;
@SerializedName("end")
public TimePoint end;
@SerializedName("reminders")
public Reminders reminders;
}
public static class TimePoint {
@SerializedName("dateTime")
public String dateTime; // Format: 2025-12-15T10:00:00+01:00
@SerializedName("date")
public String date; // Format: 2025-12-15 (for heldags)
}
public static class Reminders {
@SerializedName("useDefault")
public boolean useDefault;
@SerializedName("overrides")
public List<Override> overrides;
}
public static class Override {
@SerializedName("method")
public String method; // f.eks "popup"
@SerializedName("minutes")
public int minutes;
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\GravityEntryResponse.java
============================================================
@ -3767,7 +3959,6 @@ public class HomeFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Håndter svar på kalendertillatelse
requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
@ -3778,7 +3969,6 @@ public class HomeFragment extends Fragment {
}
}
);
// Start bakgrunnsjobb for varsling (kjører hver 15. minutt)
startNotificationWorker();
}
@ -3792,7 +3982,6 @@ public class HomeFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 0. Profil-knapp
View profileBtn = view.findViewById(R.id.btn_profile);
if (profileBtn != null) {
@ -3804,7 +3993,6 @@ public class HomeFragment extends Fragment {
calendarRecycler.setLayoutManager(new LinearLayoutManager(getContext()));
// Sett tom adapter midlertidig
calendarRecycler.setAdapter(new CalendarAdapter(new ArrayList<>(), event -> {}));
// "Se alle" knapp for kalender
TextView viewAllCalendar = view.findViewById(R.id.btn_view_all_calendar);
if (viewAllCalendar != null) {
@ -3832,7 +4020,6 @@ public class HomeFragment extends Fragment {
newsRecycler.setNestedScrollingEnabled(false);
// Sett tom adapter midlertidig
newsRecycler.setAdapter(new NewsAdapter(new ArrayList<>(), item -> {}));
// "Se alle" knapp for nyheter
TextView viewAllNews = view.findViewById(R.id.btn_view_all_news);
if (viewAllNews != null) {
@ -3848,26 +4035,23 @@ public class HomeFragment extends Fragment {
// 1. Hent personlige hendelser først (fra CalendarManager)
List<CalendarEvent> deviceEvents = CalendarManager.getDeviceEvents(getContext());
// 2. Hent API-hendelser fra WordPress
WordPressApiService apiService = RetrofitClient.getApiService();
apiService.getCalendarEvents().enqueue(new Callback<List<CalendarEvent>>() {
// 2. Hent API-hendelser DIREKTE fra Google
String url = CalendarManager.getGoogleCalendarUrl();
RetrofitClient.getApiService().getDirectGoogleEvents(url).enqueue(new Callback<GoogleCalendarModels.Response>() {
@Override
public void onResponse(Call<List<CalendarEvent>> call, Response<List<CalendarEvent>> response) {
public void onResponse(Call<GoogleCalendarModels.Response> call, Response<GoogleCalendarModels.Response> response) {
if (!isAdded()) return;
List<CalendarEvent> apiEvents = new ArrayList<>();
if (response.isSuccessful() && response.body() != null) {
for (CalendarEvent e : response.body()) {
CalendarManager.formatEventForUI(e); // Formatér datoer
apiEvents.add(e);
}
// Konverter fra Google-modell til KBS-modell
apiEvents = CalendarManager.convertGoogleResponse(response.body());
}
// 3. Flett listene (API + Personlig) og sorter
List<CalendarEvent> merged = CalendarManager.mergeAndSort(apiEvents, deviceEvents);
// 4. Filtrer ut hendelser som har passert (vis kun fremtidige + i dag)
// (CalendarManager henter 1 år bakover, så vi må filtrere for "Topp 5 kommende")
List<CalendarEvent> upcomingEvents = new ArrayList<>();
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
@ -3890,14 +4074,12 @@ public class HomeFragment extends Fragment {
}
@Override
public void onFailure(Call<List<CalendarEvent>> call, Throwable t) {
public void onFailure(Call<GoogleCalendarModels.Response> call, Throwable t) {
if (!isAdded()) return;
// Hvis API feiler, vis bare personlige events hvis vi har noen
if (!deviceEvents.isEmpty()) {
List<CalendarEvent> top5 = new ArrayList<>();
// Filtrer og plukk topp 5 fra lokale også
for(int i=0; i<Math.min(deviceEvents.size(), 5); i++) top5.add(deviceEvents.get(i));
recyclerView.setAdapter(new CalendarAdapter(top5, event -> {
CalendarDetailsBottomSheet sheet = new CalendarDetailsBottomSheet(event);
sheet.show(getParentFragmentManager(), "CalendarDetails");
@ -3931,7 +4113,8 @@ public class HomeFragment extends Fragment {
for (WpPost post : wpPosts) {
try {
Date date = rawFormat.parse(post.date);
post.date = targetFormat.format(date); // Setter pen dato
post.date = targetFormat.format(date);
// Setter pen dato
} catch (Exception e) {}
}
@ -4314,7 +4497,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java
============================================================
package com.kbs.kbsintranett;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.View;
@ -4322,6 +4512,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
@ -4346,6 +4538,7 @@ public class MainActivity extends AppCompatActivity {
bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNav, navController);
@ -4361,10 +4554,50 @@ public class MainActivity extends AppCompatActivity {
});
}
// --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) ---
checkExactAlarmPermission();
// 2. Start Silent Sign-In ved oppstart
checkLoginState();
}
/**
* Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM).
* Hvis ikke, spør brukeren om å gå til innstillinger.
*/
private void checkExactAlarmPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarmManager != null && !alarmManager.canScheduleExactAlarms()) {
// Vi mangler tillatelse. Vis dialog.
new AlertDialog.Builder(this)
.setTitle("Varslingstillatelse kreves")
.setMessage("For at kalenderen skal kunne varsle deg nøyaktig når et møte starter, må du gi appen tilgang til å sette alarmer.")
.setPositiveButton("Gå til Innstillinger", (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.setNegativeButton("Senere", null)
.show();
} else {
// Vi har tillatelse (eller er på eldre Android). Kjør logikk!
runNotificationWorker();
}
} else {
// Eldre Android-versjoner trenger ikke denne tillatelsen
runNotificationWorker();
}
}
private void runNotificationWorker() {
// --- DEBUG: TVING KJØRING AV KALENDER-SJEKK ---
// Denne linjen kjører NotificationWorker umiddelbart ved oppstart for feilsøking.
// Fjern eller kommenter ut denne når testingen er ferdig.
WorkManager.getInstance(this).enqueue(OneTimeWorkRequest.from(NotificationWorker.class));
// ----------------------------------------------
}
private void checkLoginState() {
GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);
if (account == null) {
@ -4780,13 +5013,13 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\NotificationWorker.java
============================================================
package com.kbs.kbsintranett;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.IOException;
@ -4798,8 +5031,7 @@ import retrofit2.Response;
public class NotificationWorker extends Worker {
private static final String CHANNEL_ID = "kbs_calendar_channel";
private static final String PREFS_NAME = "KBSNotificationPrefs";
private static final String TAG = "KBS_DEBUG";
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
@ -4808,73 +5040,109 @@ public class NotificationWorker extends Worker {
@NonNull
@Override
public Result doWork() {
// Dette kjører i bakgrunnen
Log.d(TAG, "NotificationWorker: Starter sjekk av kalender...");
try {
// Hent events synkront (ikke enqueue)
Response<List<CalendarEvent>> response = RetrofitClient.getApiService().getCalendarEvents().execute();
String url = CalendarManager.getGoogleCalendarUrl();
// Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) {
checkAndNotify(response.body());
List<CalendarEvent> events = CalendarManager.convertGoogleResponse(response.body());
scheduleAlarms(events);
return Result.success();
} else {
Log.e(TAG, "NotificationWorker: API-kall feilet. Kode: " + response.code());
return Result.retry();
}
} catch (IOException e) {
Log.e(TAG, "NotificationWorker: Nettverksfeil", e);
return Result.retry();
}
}
private void checkAndNotify(List<CalendarEvent> events) {
SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
long now = System.currentTimeMillis();
long fifteenMinutes = 15 * 60 * 1000;
private void scheduleAlarms(List<CalendarEvent> events) {
Context context = getApplicationContext();
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// Sjekk rettigheter (Android 12+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse! Gå til Innstillinger -> Apper -> KBS -> Alarmer og påminnelser.");
return;
}
}
long now = System.currentTimeMillis();
// Bruker en parser som er litt mer fleksibel for datoer
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
int countSet = 0;
for (CalendarEvent event : events) {
try {
Date eventDate;
if (event.getRawDate().contains("T")) eventDate = isoFormat.parse(event.getRawDate());
else eventDate = sqlFormat.parse(event.getRawDate());
// Hopp over heldagshendelser (dato uten klokkeslett)
if (event.getRawDate().length() == 10) continue;
Date eventDate = null;
// Enkel parsing. Merk: Google sender med tidssone (+01:00),
// men SimpleDateFormat uten 'X' vil parse dette som lokal tid hvis formatet stemmer.
// For optimal tidssone-håndtering burde vi brukt java.time (Android 8+),
// men dette fungerer greit så lenge telefonen er i samme sone som kalenderen.
if (event.getRawDate().contains("T")) {
// Kutter vekk tidssone-offset for enkel parsing til lokal tid
String raw = event.getRawDate();
if (raw.length() > 19) raw = raw.substring(0, 19);
eventDate = isoFormat.parse(raw);
}
if (eventDate == null) continue;
long diff = eventDate.getTime() - now;
// Beregn når alarmen skal gå
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
// Hvis eventet starter innen de neste 30 min, og ikke allerede varslet
if (diff > 0 && diff < (30 * 60 * 1000)) {
String eventId = event.getTitle() + event.getRawDate(); // Enkel ID
boolean alreadyNotified = prefs.getBoolean(eventId, false);
// Vi setter alarmen hvis tidspunktet er i fremtiden
// Vi sjekker også at det ikke er mer enn 24 timer frem i tid (for å spare ressurser)
if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
if (!alreadyNotified) {
sendNotification(event.getTitle(), "Starter kl " + event.getTime());
// Lagre at vi har varslet
prefs.edit().putBoolean(eventId, true).apply();
// Lag en unik ID for alarmen
String uniqueIdString = event.getTitle() + event.getRawDate();
int alarmId = uniqueIdString.hashCode();
Intent intent = new Intent(context, AlarmReceiver.class);
intent.putExtra("TITLE", event.getTitle());
intent.putExtra("MESSAGE", "Starter kl " + event.getTime());
intent.putExtra("ID", alarmId);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
alarmId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// VIKTIG ENDRING: Vi setter alarmen PÅ NYTT hver gang.
// AlarmManager overskriver automatisk hvis ID er lik.
// Dette sikrer at alarmen faktisk ligger der, selv etter omstart av tlf.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
}
Log.i(TAG, ">>> ALARM SATT (Oppdatert): " + event.getTitle() + " -> Skal ringe: " + new Date(triggerTime));
countSet++;
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Feil ved behandling av event: " + event.getTitle(), e);
}
}
}
private void sendNotification(String title, String content) {
NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "KBS Kalender", NotificationManager.IMPORTANCE_HIGH);
manager.createNotificationChannel(channel);
if (countSet == 0) {
Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher) // Sørg for at du har et ikon her
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
manager.notify((int) System.currentTimeMillis(), builder.build());
}
}
@ -5247,6 +5515,7 @@ import retrofit2.http.Multipart;
import retrofit2.http.Part;
import retrofit2.http.PartMap;
import retrofit2.http.Query;
import retrofit2.http.Url; // NY IMPORT
public interface WordPressApiService {
@GET("wp-json/wp/v2/posts?per_page=10&_embed")
@ -5272,9 +5541,14 @@ public interface WordPressApiService {
@Part List<MultipartBody.Part> files
);
// ENDRET: Denne brukes ikke lenger for kalender, men beholdes for bakoverkompatibilitet
@GET("wp-json/kbs/v1/calendar/events")
Call<List<CalendarEvent>> getCalendarEvents();
// NY: Direkte kall mot Google (bruker @Url for å override base URL)
@GET
Call<GoogleCalendarModels.Response> getDirectGoogleEvents(@Url String fullUrl);
@GET("wp-json/gf/v2/entries")
Call<GravityEntryResponse> getEntries(
@Query("form_ids") int formId,