Før jeg spurte aistudio

This commit is contained in:
ErolHaagenrud 2025-12-16 05:51:28 +01:00
parent d9d128245c
commit 6f3fd9059d
9 changed files with 520 additions and 250 deletions

View file

@ -16,6 +16,11 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = false
@ -26,7 +31,6 @@ android {
}
}
compileOptions {
// ENDRET: Oppgradert til Java 11 for å fikse build warnings
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

View file

@ -11,11 +11,15 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<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" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".KbsApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -30,7 +34,8 @@
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.KBSIntranett">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -39,7 +44,10 @@
<activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<receiver
android:name=".AlarmReceiver"
android:enabled="true"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -1,19 +1,22 @@
package com.kbs.kbsintranett;
import android.Manifest;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "KBS_DEBUG";
private static final String CHANNEL_ID = "kbs_calendar_channel";
private static final String CHANNEL_NAME = "KBS Kalendervarsler";
@Override
public void onReceive(Context context, Intent intent) {
@ -21,45 +24,51 @@ public class AlarmReceiver extends BroadcastReceiver {
String message = intent.getStringExtra("MESSAGE");
int notificationId = intent.getIntExtra("ID", 0);
Log.i(TAG, "AlarmReceiver: WAKE UP! Mottok alarm for: " + title);
createNotificationChannel(context);
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);
// Sjekk rettigheter for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// Vi kan ikke vise varsel uten rettighet.
return;
}
}
Intent openAppIntent = new Intent(context, MainActivity.class);
openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Intent for hva som skjer når man trykker varselet (åpne appen)
Intent tapIntent = new Intent(context, MainActivity.class);
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
tapIntent,
PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.ic_launcher_foreground) // Pass at du har et ikon her, ellers bruk 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);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.notify(notificationId, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
channel.setDescription("Varsler for kalenderhendelser i KBS Intranett");
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.CalendarContract;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -17,12 +18,10 @@ 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"));
@ -34,10 +33,9 @@ public class CalendarManager {
+ "&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
+ "&timeMin=" + timeMin;
}
// 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;
@ -53,50 +51,69 @@ public class CalendarManager {
if (item.start != null) {
if (item.start.dateTime != null) start = item.start.dateTime;
else start = item.start.date; // Heldags
else start = item.start.date;
}
if (item.end != null) {
if (item.end.dateTime != null) end = item.end.dateTime;
else end = item.end.date; // Heldags
else end = item.end.date;
}
// Varslings-logikk (Henter de sanne innstillingene)
// --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) ---
if (title.toLowerCase().contains("test")) {
Log.d("KBS_DEBUG", "--------------------------------------------------");
Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title);
Log.d("KBS_DEBUG", " Starttid: " + start);
if (item.reminders != null) {
Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault);
if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:");
for (GoogleCalendarModels.Override override : item.reminders.overrides) {
Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes);
}
} else {
Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google.");
}
} else {
Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen.");
}
Log.d("KBS_DEBUG", "--------------------------------------------------");
}
// -------------------------------------------------
// Varslings-logikk
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
}
}
reminderMinutes = 15;
} else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
// Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter
reminderMinutes = item.reminders.overrides.get(0).minutes;
}
}
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) ---
// --- RESTERENDE KODE (UENDRET) ---
// NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
return ids;
}
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 ?";
String[] selectionArgs = new String[] {"%@kbs.no"};
@ -110,7 +127,6 @@ public class CalendarManager {
if (cursor != null) {
while (cursor.moveToNext()) {
ids.add(cursor.getString(0));
// Legg til kalender-ID
}
}
} catch (Exception e) {
@ -126,17 +142,15 @@ public class CalendarManager {
return deviceEvents;
}
// 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;
}
// Hent events fra 1 år tilbake og 1 år frem
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,
@ -146,8 +160,6 @@ public class CalendarManager {
CalendarContract.Events.ALL_DAY
};
// 2. Bygg opp spørringen for å filtrere disse ID-ene
// Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?"
StringBuilder selection = new StringBuilder("(");
List<String> selectionArgsList = new ArrayList<>();
@ -166,6 +178,7 @@ 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,
@ -182,6 +195,7 @@ public class CalendarManager {
String desc = cursor.getString(3);
String loc = cursor.getString(4);
int allDay = cursor.getInt(5);
String rawStart;
String rawEnd;
@ -207,9 +221,11 @@ public class CalendarManager {
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"));
@ -219,6 +235,7 @@ 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);
@ -276,6 +293,7 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2);
});
return all;
}
}

View file

@ -0,0 +1,35 @@
package com.kbs.kbsintranett;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
public class KbsApplication extends Application {
public static final String CHANNEL_ID = "kbs_calendar_channel";
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
}
private void createNotificationChannel() {
// Vi oppretter kanalen her ved oppstart, den er klar uansett om
// det er MainActivity eller en bakgrunnsjobb som trenger den.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"KBS Kalendervarsler",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Varsler for kalenderhendelser");
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
}

View file

@ -1,9 +1,13 @@
package com.kbs.kbsintranett;
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -11,11 +15,15 @@ import android.provider.Settings;
import android.util.Log;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.work.OneTimeWorkRequest;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
@ -24,31 +32,42 @@ 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 {
// VIKTIG: Erstatt denne med din Web Client ID
// VIKTIG: Sørg for at denne matcher den du har i Google Cloud Console
public static final String GOOGLE_WEB_CLIENT_ID = "738325360287-cidl3plnqv9ei74vm9vm5muustj6eenb.apps.googleusercontent.com";
private static final String TAG = "MainActivity";
private NavController navController;
private BottomNavigationView bottomNav;
// Launcher for å spørre om varslingstillatelse (Android 13+)
private ActivityResultLauncher<String> requestPermissionLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. Setup UI
// --- 1. SETUP UI & NAVIGASJON ---
// Sjekket activity_main.xml: ID er "bottom_nav_view"
bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
if (bottomNav != null) {
NavigationUI.setupWithNavController(bottomNav, navController);
}
// Skjul meny login-skjerm
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
// Sjekker mot R.id.navigation_login som er ID'en til fragmentet
if (bottomNav == null) return;
if (destination.getId() == R.id.navigation_login) {
bottomNav.setVisibility(View.GONE);
} else {
@ -57,50 +76,32 @@ public class MainActivity extends AppCompatActivity {
});
}
// --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) ---
// --- 2. VARSLINGSOPPSETT (NYTT) ---
createNotificationChannel();
// Initialiser permission launcher for varsler
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
Log.d(TAG, "Varslingstillatelse gitt!");
} else {
Log.e(TAG, "Varslingstillatelse avslått. Bruker får ikke kalendervarsler.");
}
});
// Sjekk tillatelser (både Varsler og Alarmer)
checkNotificationPermission();
checkExactAlarmPermission();
// 2. Start Silent Sign-In ved oppstart
// Start bakgrunnsjobben for kalenderen
scheduleCalendarWork();
// --- 3. AUTENTISERING (GAMMELT) ---
checkLoginState();
}
/**
* Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM).
* Hvis ikke, spør brukeren om å til innstillinger.
* Sjekker om brukeren er logget inn med Google, og gjør en silent refresh mot WP.
*/
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) {
@ -130,10 +131,8 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onSuccess(String role) {
Log.d(TAG, "Silent login fullført. Rolle: " + role);
// videre til Home hvis vi står Login
if (navController != null && navController.getCurrentDestination() != null &&
navController.getCurrentDestination().getId() == R.id.navigation_login) {
// Denne aksjonen finnes i mobile_navigation.xml
navController.navigate(R.id.action_login_to_home);
}
}
@ -155,11 +154,69 @@ public class MainActivity extends AppCompatActivity {
private void navigateToLogin() {
if (navController != null) {
if (navController.getCurrentDestination() != null &&
// Sjekker mot R.id.navigation_login som er ID'en til fragmentet
navController.getCurrentDestination().getId() != R.id.navigation_login) {
// Denne ID'en finnes i mobile_navigation.xml
navController.navigate(R.id.navigation_login);
}
}
}
// --- NYE HJELPEMETODER FOR VARSLING ---
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "KBS Kalendervarsler";
String description = "Varsler for kalenderhendelser";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
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);
}
}
}
/**
* 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()) {
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();
}
}
}
/**
* Starter WorkManager som sjekker kalenderen hvert 15. minutt.
*/
private void scheduleCalendarWork() {
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"KbsCalendarWork",
ExistingPeriodicWorkPolicy.UPDATE,
workRequest
);
}
}

View file

@ -31,7 +31,6 @@ public class NotificationWorker extends Worker {
try {
String url = CalendarManager.getGoogleCalendarUrl();
// Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) {
@ -52,32 +51,23 @@ public class NotificationWorker extends Worker {
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.");
Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!");
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());
int countSet = 0;
for (CalendarEvent event : events) {
try {
// 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);
@ -85,14 +75,26 @@ public class NotificationWorker extends Worker {
if (eventDate == null) continue;
// Beregn når alarmen skal
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
// 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)
// --- DETALJERT LOGGING FOR Å FEILSØKE ---
if (event.getTitle().toLowerCase().contains("test")) {
Log.d(TAG, "SJEKKER TEST-EVENT:");
Log.d(TAG, " Start: " + eventDate);
Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før");
Log.d(TAG, " Alarmtid: " + new Date(triggerTime));
Log.d(TAG, " Nå: " + new Date(now));
if (triggerTime > now) {
Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)");
} else {
Log.d(TAG, " Status: I FORTIDEN (Hoppes over)");
}
}
// ----------------------------------------
if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
// Lag en unik ID for alarmen
String uniqueIdString = event.getTitle() + event.getRawDate();
int alarmId = uniqueIdString.hashCode();
@ -108,17 +110,13 @@ public class NotificationWorker extends Worker {
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));
Log.i(TAG, ">>> ALARM SATT: " + event.getTitle());
countSet++;
}
@ -128,7 +126,7 @@ public class NotificationWorker extends Worker {
}
if (countSet == 0) {
Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t).");
}
}
}

View file

@ -11,7 +11,7 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@ -22,12 +22,16 @@ public class RetrofitClient {
public static WordPressApiService getApiService() {
if (retrofit == null) {
// NYTT: Logging Interceptor
// ENDRET: Redusert loggnivå fra BODY til BASIC for å unngå spam i Logcat
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers)
if (BuildConfig.DEBUG) {
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
} else {
logging.setLevel(HttpLoggingInterceptor.Level.NONE);
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging) // Legg til loggingen her
.addInterceptor(logging)
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
@ -46,7 +50,7 @@ public class RetrofitClient {
Gson gson = new GsonBuilder()
.registerTypeAdapter(new TypeToken<List<GravityField.Choice>>(){}.getType(), new ChoicesAdapter())
.setLenient() // NYTT: Gjør parsing litt mer tilgivende
.setLenient()
.create();
retrofit = new Retrofit.Builder()

View file

@ -59,6 +59,11 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// NYTT: Dette må til for å kunne bruke BuildConfig.DEBUG i koden
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = false
@ -69,7 +74,6 @@ android {
}
}
compileOptions {
// ENDRET: Oppgradert til Java 11 for å fikse build warnings
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
@ -175,11 +179,15 @@ FILSTI: app\src\main\AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<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" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".KbsApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -194,7 +202,8 @@ FILSTI: app\src\main\AndroidManifest.xml
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true">
android:exported="true"
android:theme="@style/Theme.KBSIntranett">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -203,7 +212,10 @@ FILSTI: app\src\main\AndroidManifest.xml
<activity android:name=".WebViewActivity" />
<receiver android:name=".AlarmReceiver" android:exported="false" />
<receiver
android:name=".AlarmReceiver"
android:enabled="true"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
@ -224,20 +236,23 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\AlarmReceiver.java
============================================================
package com.kbs.kbsintranett;
import android.Manifest;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "KBS_DEBUG";
private static final String CHANNEL_ID = "kbs_calendar_channel";
private static final String CHANNEL_NAME = "KBS Kalendervarsler";
@Override
public void onReceive(Context context, Intent intent) {
@ -245,45 +260,51 @@ public class AlarmReceiver extends BroadcastReceiver {
String message = intent.getStringExtra("MESSAGE");
int notificationId = intent.getIntExtra("ID", 0);
Log.i(TAG, "AlarmReceiver: WAKE UP! Mottok alarm for: " + title);
createNotificationChannel(context);
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);
// Sjekk rettigheter for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// Vi kan ikke vise varsel uten rettighet.
return;
}
}
Intent openAppIntent = new Intent(context, MainActivity.class);
openAppIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Intent for hva som skjer når man trykker på varselet (åpne appen)
Intent tapIntent = new Intent(context, MainActivity.class);
tapIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
tapIntent,
PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.ic_launcher_foreground) // Pass på at du har et ikon her, ellers bruk 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);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.notify(notificationId, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance);
channel.setDescription("Varsler for kalenderhendelser i KBS Intranett");
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
}
@ -742,6 +763,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.CalendarContract;
import android.util.Log;
import androidx.core.content.ContextCompat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -755,12 +777,10 @@ 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"));
@ -772,10 +792,9 @@ public class CalendarManager {
+ "&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
+ "&timeMin=" + timeMin;
}
// 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;
@ -791,50 +810,69 @@ public class CalendarManager {
if (item.start != null) {
if (item.start.dateTime != null) start = item.start.dateTime;
else start = item.start.date; // Heldags
else start = item.start.date;
}
if (item.end != null) {
if (item.end.dateTime != null) end = item.end.dateTime;
else end = item.end.date; // Heldags
else end = item.end.date;
}
// Varslings-logikk (Henter de sanne innstillingene)
// --- SPY LOGGING (Se i Logcat etter KBS_DEBUG) ---
if (title.toLowerCase().contains("test")) {
Log.d("KBS_DEBUG", "--------------------------------------------------");
Log.d("KBS_DEBUG", "ANALYSERER EVENT: " + title);
Log.d("KBS_DEBUG", " Starttid: " + start);
if (item.reminders != null) {
Log.d("KBS_DEBUG", " useDefault: " + item.reminders.useDefault);
if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
Log.d("KBS_DEBUG", " Fant " + item.reminders.overrides.size() + " overstyringer:");
for (GoogleCalendarModels.Override override : item.reminders.overrides) {
Log.d("KBS_DEBUG", " -> Metode: '" + override.method + "', Minutter: " + override.minutes);
}
} else {
Log.d("KBS_DEBUG", " Ingen 'overrides' (spesifikke varsler) funnet i listen fra Google.");
}
} else {
Log.d("KBS_DEBUG", " Ingen 'reminders'-seksjon funnet i JSON-responsen.");
}
Log.d("KBS_DEBUG", "--------------------------------------------------");
}
// -------------------------------------------------
// Varslings-logikk
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
}
}
reminderMinutes = 15;
} else if (item.reminders.overrides != null && !item.reminders.overrides.isEmpty()) {
// Vi tar den første vi finner for å se om det fanger opp dine 10/26 minutter
reminderMinutes = item.reminders.overrides.get(0).minutes;
}
}
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) ---
// --- RESTERENDE KODE (UENDRET) ---
// NY HJELPEMETODE: Finner ID-ene til kalendere som tilhører @kbs.no
private static List<String> getKbsCalendarIds(Context context) {
List<String> ids = new ArrayList<>();
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
return ids;
}
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 ?";
String[] selectionArgs = new String[] {"%@kbs.no"};
@ -848,7 +886,6 @@ public class CalendarManager {
if (cursor != null) {
while (cursor.moveToNext()) {
ids.add(cursor.getString(0));
// Legg til kalender-ID
}
}
} catch (Exception e) {
@ -864,17 +901,15 @@ public class CalendarManager {
return deviceEvents;
}
// 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;
}
// Hent events fra 1 år tilbake og 1 år frem
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,
@ -884,8 +919,6 @@ public class CalendarManager {
CalendarContract.Events.ALL_DAY
};
// 2. Bygg opp spørringen for å filtrere på disse ID-ene
// Resultatet blir noe sånt som: "((calendar_id = ?) OR (calendar_id = ?)) AND dtstart >= ? AND dtstart <= ?"
StringBuilder selection = new StringBuilder("(");
List<String> selectionArgsList = new ArrayList<>();
@ -904,6 +937,7 @@ 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,
@ -920,6 +954,7 @@ public class CalendarManager {
String desc = cursor.getString(3);
String loc = cursor.getString(4);
int allDay = cursor.getInt(5);
String rawStart;
String rawEnd;
@ -945,9 +980,11 @@ public class CalendarManager {
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"));
@ -957,6 +994,7 @@ 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);
@ -1014,6 +1052,7 @@ public class CalendarManager {
String d2 = e2.getRawDate() != null ? e2.getRawDate() : "";
return d1.compareTo(d2);
});
return all;
}
}
@ -4325,6 +4364,45 @@ public class InternalLinkMovementMethod extends LinkMovementMethod {
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\KbsApplication.java
============================================================
package com.kbs.kbsintranett;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Build;
public class KbsApplication extends Application {
public static final String CHANNEL_ID = "kbs_calendar_channel";
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
}
private void createNotificationChannel() {
// Vi oppretter kanalen her ved oppstart, så den er klar uansett om
// det er MainActivity eller en bakgrunnsjobb som trenger den.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"KBS Kalendervarsler",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Varsler for kalenderhendelser");
NotificationManager manager = getSystemService(NotificationManager.class);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
}
============================================================
FILSTI: app\src\main\java\com\kbs\kbsintranett\LoginFragment.java
============================================================
@ -4497,10 +4575,14 @@ FILSTI: app\src\main\java\com\kbs\kbsintranett\MainActivity.java
============================================================
package com.kbs.kbsintranett;
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -4508,11 +4590,15 @@ import android.provider.Settings;
import android.util.Log;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import androidx.work.OneTimeWorkRequest;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
@ -4521,31 +4607,42 @@ 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 {
// VIKTIG: Erstatt denne med din Web Client ID
// VIKTIG: Sørg for at denne matcher den du har i Google Cloud Console
public static final String GOOGLE_WEB_CLIENT_ID = "SECRET.apps.googleusercontent.com";
private static final String TAG = "MainActivity";
private NavController navController;
private BottomNavigationView bottomNav;
// Launcher for å spørre om varslingstillatelse (Android 13+)
private ActivityResultLauncher<String> requestPermissionLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. Setup UI
// --- 1. SETUP UI & NAVIGASJON ---
// Sjekket activity_main.xml: ID er "bottom_nav_view"
bottomNav = findViewById(R.id.bottom_nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
.findFragmentById(R.id.nav_host_fragment);
if (navHostFragment != null) {
navController = navHostFragment.getNavController();
if (bottomNav != null) {
NavigationUI.setupWithNavController(bottomNav, navController);
}
// Skjul meny på login-skjerm
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
// Sjekker mot R.id.navigation_login som er ID'en til fragmentet
if (bottomNav == null) return;
if (destination.getId() == R.id.navigation_login) {
bottomNav.setVisibility(View.GONE);
} else {
@ -4554,50 +4651,32 @@ public class MainActivity extends AppCompatActivity {
});
}
// --- NYTT: Sjekk tillatelse for nøyaktige alarmer (Android 12+) ---
// --- 2. VARSLINGSOPPSETT (NYTT) ---
createNotificationChannel();
// Initialiser permission launcher for varsler
requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
Log.d(TAG, "Varslingstillatelse gitt!");
} else {
Log.e(TAG, "Varslingstillatelse avslått. Bruker får ikke kalendervarsler.");
}
});
// Sjekk tillatelser (både Varsler og Alarmer)
checkNotificationPermission();
checkExactAlarmPermission();
// 2. Start Silent Sign-In ved oppstart
// Start bakgrunnsjobben for kalenderen
scheduleCalendarWork();
// --- 3. AUTENTISERING (GAMMELT) ---
checkLoginState();
}
/**
* Sjekker om appen har lov til å sette nøyaktige alarmer (SCHEDULE_EXACT_ALARM).
* Hvis ikke, spør brukeren om å gå til innstillinger.
* Sjekker om brukeren er logget inn med Google, og gjør en silent refresh mot WP.
*/
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) {
@ -4627,10 +4706,8 @@ public class MainActivity extends AppCompatActivity {
@Override
public void onSuccess(String role) {
Log.d(TAG, "Silent login fullført. Rolle: " + role);
// Gå videre til Home hvis vi står på Login
if (navController != null && navController.getCurrentDestination() != null &&
navController.getCurrentDestination().getId() == R.id.navigation_login) {
// Denne aksjonen finnes i mobile_navigation.xml
navController.navigate(R.id.action_login_to_home);
}
}
@ -4652,13 +4729,71 @@ public class MainActivity extends AppCompatActivity {
private void navigateToLogin() {
if (navController != null) {
if (navController.getCurrentDestination() != null &&
// Sjekker mot R.id.navigation_login som er ID'en til fragmentet
navController.getCurrentDestination().getId() != R.id.navigation_login) {
// Denne ID'en finnes i mobile_navigation.xml
navController.navigate(R.id.navigation_login);
}
}
}
// --- NYE HJELPEMETODER FOR VARSLING ---
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "KBS Kalendervarsler";
String description = "Varsler for kalenderhendelser";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel("kbs_calendar_channel", name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
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);
}
}
}
/**
* 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()) {
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();
}
}
}
/**
* Starter WorkManager som sjekker kalenderen hvert 15. minutt.
*/
private void scheduleCalendarWork() {
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"KbsCalendarWork",
ExistingPeriodicWorkPolicy.UPDATE,
workRequest
);
}
}
============================================================
@ -5044,7 +5179,6 @@ public class NotificationWorker extends Worker {
try {
String url = CalendarManager.getGoogleCalendarUrl();
// Hent events synkront
Response<GoogleCalendarModels.Response> response = RetrofitClient.getApiService().getDirectGoogleEvents(url).execute();
if (response.isSuccessful() && response.body() != null) {
@ -5065,32 +5199,23 @@ public class NotificationWorker extends Worker {
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.");
Log.e(TAG, "NotificationWorker: MANGLER fortsatt tillatelse!");
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());
int countSet = 0;
for (CalendarEvent event : events) {
try {
// 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);
@ -5098,14 +5223,26 @@ public class NotificationWorker extends Worker {
if (eventDate == null) continue;
// Beregn når alarmen skal gå
long triggerTime = eventDate.getTime() - (event.getReminderMinutes() * 60 * 1000L);
// 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)
// --- DETALJERT LOGGING FOR Å FEILSØKE ---
if (event.getTitle().toLowerCase().contains("test")) {
Log.d(TAG, "SJEKKER TEST-EVENT:");
Log.d(TAG, " Start: " + eventDate);
Log.d(TAG, " Varsling valgt: " + event.getReminderMinutes() + " min før");
Log.d(TAG, " Alarmtid: " + new Date(triggerTime));
Log.d(TAG, " Nå: " + new Date(now));
if (triggerTime > now) {
Log.d(TAG, " Status: I FREMTIDEN (Alarm skal settes)");
} else {
Log.d(TAG, " Status: I FORTIDEN (Hoppes over)");
}
}
// ----------------------------------------
if (triggerTime > now && triggerTime < (now + 24 * 60 * 60 * 1000L)) {
// Lag en unik ID for alarmen
String uniqueIdString = event.getTitle() + event.getRawDate();
int alarmId = uniqueIdString.hashCode();
@ -5121,17 +5258,13 @@ public class NotificationWorker extends Worker {
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));
Log.i(TAG, ">>> ALARM SATT: " + event.getTitle());
countSet++;
}
@ -5141,7 +5274,7 @@ public class NotificationWorker extends Worker {
}
if (countSet == 0) {
Log.d(TAG, "Ingen kommende alarmer (innenfor neste 24t) funnet akkurat nå.");
Log.d(TAG, "Ingen nye alarmer satt (ingen hendelser innenfor neste 24t).");
}
}
}
@ -5260,7 +5393,7 @@ import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor; // NY IMPORT
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@ -5271,12 +5404,16 @@ public class RetrofitClient {
public static WordPressApiService getApiService() {
if (retrofit == null) {
// NYTT: Logging Interceptor
// ENDRET: Redusert loggnivå fra BODY til BASIC for å unngå spam i Logcat
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY); // Logger ALT (Body, Headers)
if (BuildConfig.DEBUG) {
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
} else {
logging.setLevel(HttpLoggingInterceptor.Level.NONE);
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging) // Legg til loggingen her
.addInterceptor(logging)
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
@ -5295,7 +5432,7 @@ public class RetrofitClient {
Gson gson = new GsonBuilder()
.registerTypeAdapter(new TypeToken<List<GravityField.Choice>>(){}.getType(), new ChoicesAdapter())
.setLenient() // NYTT: Gjør parsing litt mer tilgivende
.setLenient()
.create();
retrofit = new Retrofit.Builder()