mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 20:34:02 +01:00
The decision layer needed a runtime. A polling loop was the obvious design and the wrong one: Doze can skip a poll indefinitely, and skipping the 05:00 boundary leaves the curfew allowlist applied into the morning -- the user finds their banking app missing with no obvious cause. The transitions are known ahead of time, so they get exact alarms. The service wakes, applies, schedules the next run and stops rather than sitting resident. BootReceiver takes LOCKED_BOOT_COMPLETED as well as BOOT_COMPLETED, because the latter does not arrive until first unlock. That is not theoretical: this phone sat at RUNNING_LOCKED for minutes after an earlier reboot in this session, with no network and no app able to start. Relying on BOOT_COMPLETED alone freezes enforcement state across a reboot -- safe for hiding, not for unhiding, which is the direction that strands you without a dialer. The schedule is re-armed before unlock; the pass itself waits until storage is readable. The first on-device run logged "exact alarms not permitted", and dumpsys showed what that costs: window=+11m14s, maxWhenElapsed=+25m51s, so a 15-minute fallback could slip to 26 minutes. SCHEDULE_EXACT_ALARM is user-revocable and denied by default on Android 16; USE_EXACT_ALARM is the permission for apps whose function is acting at specific times, and is granted at install. After the change: window=0, exactAllowReason=policy_permission, whenElapsed equal to maxWhenElapsed. The service reports "not DO/PO - no enforcement applied" and does nothing when unprovisioned, rather than failing quietly, and a failure inside a pass cannot stop the next run being scheduled -- an enforcement bug should degrade one tick, not end the chain. Not yet wired: the service logs a tick rather than calling setApplicationHidden with the decision. Both halves are tested; joining them is next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
74 lines
2.5 KiB
Dart
74 lines
2.5 KiB
Dart
import 'package:flutter/services.dart';
|
|
|
|
/// Provisioning state reported by the native side.
|
|
class DevicePolicyStatus {
|
|
const DevicePolicyStatus({
|
|
required this.packageName,
|
|
required this.isDeviceOwner,
|
|
required this.isAdminActive,
|
|
required this.sdkInt,
|
|
required this.restrictionsApplied,
|
|
});
|
|
|
|
/// Builds a status from the raw platform-channel map.
|
|
factory DevicePolicyStatus.fromMap(Map<Object?, Object?> map) {
|
|
return DevicePolicyStatus(
|
|
packageName: map['packageName']! as String,
|
|
isDeviceOwner: map['isDeviceOwner']! as bool,
|
|
isAdminActive: map['isAdminActive']! as bool,
|
|
sdkInt: map['sdkInt']! as int,
|
|
restrictionsApplied: map['restrictionsApplied']! as bool,
|
|
);
|
|
}
|
|
|
|
final String packageName;
|
|
final bool isDeviceOwner;
|
|
final bool isAdminActive;
|
|
final int sdkInt;
|
|
|
|
/// Whether any user restriction is currently enforced.
|
|
///
|
|
/// Always false in this build: the app is provisioning-capable but inert
|
|
/// until the release path has been verified on a real device.
|
|
final bool restrictionsApplied;
|
|
}
|
|
|
|
/// Dart side of the device-policy platform channel.
|
|
class DevicePolicy {
|
|
const DevicePolicy([this.channel = _defaultChannel]);
|
|
|
|
static const MethodChannel _defaultChannel =
|
|
MethodChannel('com.kuhy.focus_owner/device_policy');
|
|
|
|
final MethodChannel channel;
|
|
|
|
/// Reads the current provisioning state.
|
|
Future<DevicePolicyStatus> status() async {
|
|
final result =
|
|
await channel.invokeMethod<Map<Object?, Object?>>('status');
|
|
return DevicePolicyStatus.fromMap(result!);
|
|
}
|
|
|
|
/// Runs one enforcement pass now and arms the schedule.
|
|
///
|
|
/// The service schedules the following run at the end of each pass, so this
|
|
/// is also how the chain is started on a device that has not rebooted since
|
|
/// the app was installed.
|
|
Future<bool> runEnforcementNow() async =>
|
|
await channel.invokeMethod<bool>('runEnforcementNow') ?? false;
|
|
|
|
/// Cancels any pending scheduled evaluation.
|
|
Future<bool> cancelEnforcement() async =>
|
|
await channel.invokeMethod<bool>('cancelEnforcement') ?? false;
|
|
|
|
/// Relinquishes device ownership without wiping the device.
|
|
///
|
|
/// This is the escape hatch. It is deliberately reachable from the app's
|
|
/// main screen with no PC attached, because the situation it exists for is
|
|
/// one where ADB may already be disallowed.
|
|
Future<bool> releaseDeviceOwner() async {
|
|
final released = await channel.invokeMethod<bool>('releaseDeviceOwner');
|
|
return released ?? false;
|
|
}
|
|
}
|