Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.exptech.dpip

import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.net.Uri
Expand Down Expand Up @@ -29,10 +30,62 @@ class PermissionSettingsChannel(private val context: Context) :
when (call.method) {
"backgroundLocationOptionLabel" -> result.success(backgroundLocationOptionLabel())
"openNotificationSettings" -> result.success(openNotificationSettings())
"urgentNotificationChannelStatus" -> urgentNotificationChannelStatus(call, result)
"openNotificationChannelSettings" -> openNotificationChannelSettings(call, result)
else -> result.notImplemented()
}
}

private fun urgentNotificationChannelStatus(call: MethodCall, result: MethodChannel.Result) {
val channelIds = call.argument<List<*>>("channelIds")
if (channelIds == null || channelIds.isEmpty() || channelIds.any { it !is String || it.isBlank() }) {
result.error("invalid_channel_ids", "channelIds must be non-empty strings", null)
return
}
val manager = context.getSystemService(NotificationManager::class.java)
val statuses = channelIds.associate { rawId ->
val id = rawId as String
val channel = manager.getNotificationChannel(id)
id to channel?.let {
mapOf(
"id" to it.id,
"name" to it.name.toString(),
"bypassesDnd" to it.canBypassDnd(),
)
}
}
result.success(statuses)
}

private fun openNotificationChannelSettings(call: MethodCall, result: MethodChannel.Result) {
val channelId = call.argument<String>("channelId")
if (channelId.isNullOrBlank()) {
result.error("invalid_channel_id", "channelId must be a non-empty string", null)
return
}
val manager = context.getSystemService(NotificationManager::class.java)
if (manager.getNotificationChannel(channelId) == null) {
result.error("unknown_channel", "Notification channel does not exist", null)
return
}
result.success(openNotificationChannelSettings(channelId))
}

private fun openNotificationChannelSettings(channelId: String): String {
try {
context.startActivity(
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
return "channel"
} catch (e: Exception) {
Log.i(TAG, "channel notification settings unavailable", e)
}
return openNotificationSettings()
}

private fun backgroundLocationOptionLabel(): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
return context.packageManager.backgroundPermissionOptionLabel.toString()
Expand Down
12 changes: 12 additions & 0 deletions lib/core/notifications/notification_channels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,18 @@ abstract final class NotificationChannels {
),
];

/// Android channels whose system settings should be reviewed for Do Not
/// Disturb bypass. The two general EEW channels also use `criticalAlerts`,
/// but are not major alerts.
static const Set<String> urgentChannelKeys = {
'eew_alert-important-v2',
'eew-important-v2',
'thunderstorm-important-v2',
'weather_major-important-v2',
'evacuation_major-important-v2',
'tsunami-important-v2',
};

/// What [channel] will actually do when it fires.
///
/// Derived from the definition rather than written out beside it, so a
Expand Down
125 changes: 125 additions & 0 deletions lib/core/notifications/urgent_notification_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/// Reads Android Do Not Disturb bypass status for DPIP's urgent notification
/// channels and opens their system settings.
library;

import 'package:dpip/core/logging/log.dart';
import 'package:dpip/core/notifications/notification_channels.dart';
import 'package:flutter/services.dart';

enum UrgentNotificationChannelState {
missing,
doesNotBypass,
bypasses,
unavailable,
}

class UrgentNotificationChannelStatus {
const UrgentNotificationChannelStatus({
required this.requestedId,
required this.channelId,
required this.name,
required this.state,
});

final String requestedId;
final String? channelId;
final String? name;
final UrgentNotificationChannelState state;
}

class UrgentNotificationStatus {
const UrgentNotificationStatus(this.channels);

final List<UrgentNotificationChannelStatus> channels;

bool get allBypass =>
channels.isNotEmpty &&
channels.every(
(channel) => channel.state == UrgentNotificationChannelState.bypasses,
);
}

class UrgentNotificationSettings {
UrgentNotificationSettings([MethodChannel? channel])
: _channel =
channel ??
const MethodChannel('com.exptech.dpip/permission_settings');

final MethodChannel _channel;

Future<UrgentNotificationStatus> status() async {
final keys = NotificationChannels.urgentChannelKeys.toList(growable: false);
try {
final response = await _channel.invokeMapMethod<String, Object?>(
'urgentNotificationChannelStatus',
{'channelIds': keys},
);
return UrgentNotificationStatus([
for (final key in keys) _parse(key, response?[key]),
]);
} on PlatformException catch (error, stackTrace) {
Log.handle(error, stackTrace, 'urgent notification channel status');
return _unavailable(keys);
} on MissingPluginException {
return _unavailable(keys);
}
}

Future<String> openChannelSettings(String channelId) async {
try {
return await _channel.invokeMethod<String>(
'openNotificationChannelSettings',
{'channelId': channelId},
) ??
'none';
} on PlatformException catch (error, stackTrace) {
Log.handle(error, stackTrace, 'urgent notification channel settings');
return 'none';
} on MissingPluginException {
return 'none';
}
}

UrgentNotificationChannelStatus _parse(String key, Object? value) {
if (value == null) {
return UrgentNotificationChannelStatus(
requestedId: key,
channelId: null,
name: null,
state: UrgentNotificationChannelState.missing,
);
}
if (value is! Map<Object?, Object?>) return _unavailableChannel(key);
final id = value['id'];
final name = value['name'];
final bypasses = value['bypassesDnd'];
if (id is! String ||
id.isEmpty ||
name is! String ||
name.isEmpty ||
bypasses is! bool) {
return _unavailableChannel(key);
}
return UrgentNotificationChannelStatus(
requestedId: key,
channelId: id,
name: name,
state: bypasses
? UrgentNotificationChannelState.bypasses
: UrgentNotificationChannelState.doesNotBypass,
);
}

UrgentNotificationStatus _unavailable(List<String> keys) =>
UrgentNotificationStatus([
for (final key in keys) _unavailableChannel(key),
]);

UrgentNotificationChannelStatus _unavailableChannel(String key) =>
UrgentNotificationChannelStatus(
requestedId: key,
channelId: null,
name: null,
state: UrgentNotificationChannelState.unavailable,
);
}
10 changes: 10 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,16 @@
},
"reportFilterTitle": "Filters",
"onboardingPermCritical": "Critical alerts",
"onboardingPermUrgentAndroid": "Major alerts in Do Not Disturb",
"@onboardingPermUrgentAndroid": {"description": "Android permission-check row for urgent notification channels"},
"onboardingPermUrgentAndroidDesc": "Choose which major-alert channels Android may allow during Do Not Disturb. Silent mode, volume, and the active Mode still apply.",
"@onboardingPermUrgentAndroidDesc": {"description": "Limitations of Android urgent notification channel settings"},
"permissionGuideUrgentAndroid": "In the channel settings, choose whether this notification may interrupt Do Not Disturb, then return to DPIP.",
"@permissionGuideUrgentAndroid": {"description": "Instruction before opening one Android notification channel"},
"urgentNotificationBypassesDnd": "Allowed during Do Not Disturb",
"@urgentNotificationBypassesDnd": {"description": "Android notification channel currently bypasses Do Not Disturb"},
"urgentNotificationFollowsDnd": "Follows Do Not Disturb",
"@urgentNotificationFollowsDnd": {"description": "Android notification channel currently follows Do Not Disturb"},
"trendCumulativeTotal": "Cumulative {total} mm",
"languageName": "English",
"@mapLayerStyleJma": {
Expand Down
5 changes: 5 additions & 0 deletions lib/l10n/app_fil.arb
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@
"onboardingTermsBody": "Mangyaring basahin ang mga sumusunod na paunawa bago gamitin ang DPIP:\n\n• Ang lahat ng impormasyon ay dapat sumunod sa nilalamang inilathala ng Central Weather Administration (CWA).\n\n• Depende sa kalagayan ng network, server, app, at pinagmumulan ng datos, may posibilidad na hindi matanggap ang impormasyon; ginagawa namin ang lahat ng aming makakaya upang maiwasan ito ngunit hindi namin magagarantiya na hindi ito mangyayari.\n\n• Maaaring maunang makarating sa iyong lokasyon ang malakas na pagyanig bago pa dumating ang notipikasyon.\n\n• Ang mga maagang babala sa lindol ay mabilis na kinakalkulang resulta na maaaring magtaglay ng malaking pagkakamali — unawain ito at gamitin nang may pag-iingat.\n\n• Anumang gawaing hindi pinahihintulutan ng mga awtoridad ay maaaring magdala ng panganib sa batas; mangyaring sundin ang lahat ng naaangkop na regulasyon.\n\nBukod dito, upang magbigay ng lokal na mga alerto, kinokolekta at ini-upload ng serbisyong ito ang iyong tinatayang lokasyon at push identifier — sa foreground at background — para lamang matukoy kung aling mga alerto ang ipapadala sa iyo.\n\nSa pamamagitan ng pag-tap sa \"Sumang-ayon at magpatuloy\" kinukumpirma mo na nabasa, naunawaan, at sinasang-ayunan mo ang nasa itaas.",
"reportFilterTitle": "Mga filter",
"onboardingPermCritical": "Mga kritikal na alerto",
"onboardingPermUrgentAndroid": "Mahahalagang alerto sa Do Not Disturb",
"onboardingPermUrgentAndroidDesc": "Suriin ang bawat mahalagang channel para payagan ito ng Android sa Do Not Disturb. Nalalapat pa rin ang silent mode, volume, at kasalukuyang Mode.",
"permissionGuideUrgentAndroid": "Sa channel settings, piliin kung maaaring gambalain ng notification na ito ang Do Not Disturb, pagkatapos ay bumalik sa DPIP.",
"urgentNotificationBypassesDnd": "Pinapayagan sa Do Not Disturb",
"urgentNotificationFollowsDnd": "Sumusunod sa Do Not Disturb",
"trendCumulativeTotal": "Kabuuang {total} mm",
"languageName": "Filipino",
"reportListEmptyFiltered": "Walang ulat na tumutugma sa mga filter",
Expand Down
5 changes: 5 additions & 0 deletions lib/l10n/app_id.arb
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@
"onboardingTermsBody": "Harap baca pemberitahuan berikut sebelum menggunakan DPIP:\n\n• Semua informasi harus mengacu pada konten yang diterbitkan oleh Central Weather Administration (CWA) Taiwan.\n\n• Bergantung pada kondisi jaringan, server, aplikasi, dan sumber data hulu, informasi mungkin tidak diterima; kami berupaya sebaik mungkin untuk menghindari hal ini tetapi tidak dapat menjamin bahwa hal itu tidak akan pernah terjadi.\n\n• Guncangan kuat dapat mencapai lokasi Anda sebelum notifikasi tiba.\n\n• Peringatan dini gempa adalah hasil perhitungan cepat yang mungkin mengandung kesalahan yang signifikan — pahami hal ini dan gunakan dengan hati-hati.\n\n• Setiap tindakan yang tidak disahkan oleh pihak berwenang dapat menimbulkan risiko hukum; harap patuhi semua peraturan yang berlaku.\n\nSelain itu, untuk menyediakan peringatan yang dilokalkan, layanan ini mengumpulkan dan mengunggah perkiraan lokasi Anda dan pengidentifikasi push — di latar depan maupun latar belakang — semata-mata untuk menentukan peringatan mana yang akan dikirimkan kepada Anda.\n\nDengan mengetuk \"Setuju dan lanjutkan\", Anda mengonfirmasi bahwa Anda telah membaca, memahami, dan menyetujui hal-hal di atas.",
"reportFilterTitle": "Filter",
"onboardingPermCritical": "Peringatan kritis",
"onboardingPermUrgentAndroid": "Peringatan penting saat Jangan Ganggu",
"onboardingPermUrgentAndroidDesc": "Periksa setiap saluran peringatan penting agar Android dapat mengizinkannya saat Jangan Ganggu. Mode senyap, volume, dan Mode aktif tetap berlaku.",
"permissionGuideUrgentAndroid": "Di setelan saluran, pilih apakah notifikasi ini boleh menginterupsi Jangan Ganggu, lalu kembali ke DPIP.",
"urgentNotificationBypassesDnd": "Diizinkan saat Jangan Ganggu",
"urgentNotificationFollowsDnd": "Mengikuti Jangan Ganggu",
"trendCumulativeTotal": "Total {total} mm",
"languageName": "Bahasa Indonesia",
"reportListEmptyFiltered": "Tidak ada laporan yang cocok dengan filter",
Expand Down
5 changes: 5 additions & 0 deletions lib/l10n/app_ja.arb
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@
"onboardingTermsBody": "DPIP をご利用になる前に、以下の注意事項を必ずお読みください:\n\n• すべての情報は、台湾中央気象署(CWA)が発表する内容を優先してください。\n\n• ネットワーク、サーバー、アプリ、上流のデータソースの状態によっては、情報を受信できない場合があります。可能な限り回避に努めますが、決して発生しないことを保証するものではありません。\n\n• 強い揺れが、通知より先にあなたの所在地へ到達する場合があります。\n\n• 緊急地震速報は高速に計算された結果であり、大きな誤差を含む可能性があります。この点を理解したうえで、慎重にご利用ください。\n\n• 公的機関に認められていない行為には法的リスクが伴う可能性があります。関連する規定を必ずお守りください。\n\nまた、地域に応じた警報を提供するため、本サービスは、どの警報をあなたに送信するかを判断する目的にのみ、あなたのおおよその位置情報とプッシュ識別子を、フォアグラウンドおよびバックグラウンドで収集・アップロードします。\n\n下部の「同意して続行」をタップすることで、上記を読み、理解し、同意したものとみなされます。",
"reportFilterTitle": "絞り込み",
"onboardingPermCritical": "重大な通知",
"onboardingPermUrgentAndroid": "おやすみモード中の重大通知",
"onboardingPermUrgentAndroidDesc": "重大通知の各チャンネルを確認し、Android でおやすみモード中の通知を許可します。消音、音量、現在のモード設定は引き続き適用されます。",
"permissionGuideUrgentAndroid": "チャンネル設定で、この通知がおやすみモードを中断できるかを選択してから、DPIP に戻ってください。",
"urgentNotificationBypassesDnd": "おやすみモード中も許可",
"urgentNotificationFollowsDnd": "おやすみモードに従う",
"trendCumulativeTotal": "累計 {total} mm",
"languageName": "日本語",
"reportListEmptyFiltered": "条件に一致する地震報告はありません",
Expand Down
5 changes: 5 additions & 0 deletions lib/l10n/app_ko.arb
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@
"onboardingTermsBody": "DPIP를 사용하기 전에 다음 유의 사항을 반드시 읽어 주세요:\n\n• 모든 정보는 중앙기상청(CWA)에서 발표한 내용을 기준으로 합니다.\n\n• 네트워크, 서버, 애플리케이션, 상위 데이터 출처의 상태에 따라 정보를 받지 못할 수 있습니다. 이러한 상황을 방지하기 위해 최선을 다하고 있으나, 절대 발생하지 않는다고 보장할 수는 없습니다.\n\n• 강한 흔들림이 알림보다 먼저 귀하의 위치에 도달할 수 있습니다.\n\n• 지진 조기경보는 빠르게 계산된 결과로 상당한 오차가 있을 수 있으므로, 이를 이해하고 신중하게 사용하시기 바랍니다.\n\n• 당국이 승인하지 않은 모든 행위는 법적 위험을 수반할 수 있으니, 관련 규정을 반드시 준수해 주세요.\n\n또한 지역 맞춤형 경보를 제공하기 위해, 본 서비스는 귀하에게 어떤 경보를 보낼지 결정하기 위한 목적으로만 포그라운드 및 백그라운드에서 귀하의 대략적인 위치와 푸시 식별자를 수집하여 업로드합니다.\n\n하단의 “동의하고 계속”을 누르면 위 사항을 읽고 이해했으며 이에 동의함을 확인하는 것입니다.",
"reportFilterTitle": "필터",
"onboardingPermCritical": "중요 알림",
"onboardingPermUrgentAndroid": "방해 금지 모드의 중요 알림",
"onboardingPermUrgentAndroidDesc": "각 중요 알림 채널을 확인하여 Android가 방해 금지 모드에서도 허용하도록 설정하세요. 무음, 음량 및 현재 모드 설정은 계속 적용됩니다.",
"permissionGuideUrgentAndroid": "채널 설정에서 이 알림이 방해 금지 모드를 중단할 수 있는지 선택한 후 DPIP로 돌아오세요.",
"urgentNotificationBypassesDnd": "방해 금지 모드에서도 허용",
"urgentNotificationFollowsDnd": "방해 금지 모드 따름",
"trendCumulativeTotal": "누적 {total} mm",
"languageName": "한국어",
"reportListEmptyFiltered": "조건에 맞는 지진 보고서가 없습니다",
Expand Down
Loading
Loading