thl_ussd_service 0.2.0
thl_ussd_service: ^0.2.0 copied to clipboard
A Flutter plugin to make silent USSD requests and read their responses, using Android's sendUssdRequest. iOS is not supported. Upgraded fork of ussd_service.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:thl_ussd_service/thl_ussd_service.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'THL USSD Service Demo',
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF1976D2),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF1976D2),
secondary: Color(0xFF03DAC6),
),
),
home: const UssdDemoPage(),
);
}
}
class UssdDemoPage extends StatefulWidget {
const UssdDemoPage({Key? key}) : super(key: key);
@override
_UssdDemoPageState createState() => _UssdDemoPageState();
}
enum UssdMode {
silent,
interactiveSingle,
interactiveMulti,
}
class _UssdDemoPageState extends State<UssdDemoPage> {
UssdMode _ussdMode = UssdMode.silent;
bool _isAccessibilityEnabled = false;
bool _isOverlayPermissionGranted = false;
List<Map<String, dynamic>> _simCards = [];
int _selectedSimSubscriptionId = 0;
int _selectedSimSlotIndex = 0;
final TextEditingController _codeController = TextEditingController(text: '*123#');
final TextEditingController _optionsController = TextEditingController(text: '1, 2, 3');
final TextEditingController _overlayMessageController = TextEditingController(text: 'USSD session running...');
bool _loading = false;
String _statusMessage = 'Ready';
final List<String> _messageLogs = [];
@override
void initState() {
super.initState();
_checkStatusAndSims();
UssdService.setUssdMessageListener((message) {
setState(() {
_messageLogs.add('Received: $message');
});
});
}
@override
void dispose() {
UssdService.removeUssdMessageListener();
_codeController.dispose();
_optionsController.dispose();
_overlayMessageController.dispose();
super.dispose();
}
Future<void> _checkStatusAndSims() async {
final access = await UssdService.isAccessibilityEnabled();
final overlay = await UssdService.isOverlayPermissionGranted();
await Permission.phone.request();
final sims = await UssdService.getSimCards();
setState(() {
_isAccessibilityEnabled = access;
_isOverlayPermissionGranted = overlay;
_simCards = sims;
if (_simCards.isNotEmpty) {
_selectedSimSubscriptionId = _simCards.first['subscriptionId'] ?? 0;
_selectedSimSlotIndex = _simCards.first['slotIndex'] ?? 0;
}
});
}
Future<void> _executeUssd() async {
setState(() {
_loading = true;
_messageLogs.clear();
_statusMessage = 'Initiating request...';
});
final code = _codeController.text.trim();
try {
if (_ussdMode == UssdMode.silent) {
_statusMessage = 'Running silent request...';
final response = await UssdService.makeRequest(_selectedSimSubscriptionId, code);
setState(() {
_statusMessage = 'Success';
_messageLogs.add('Response: $response');
});
} else if (_ussdMode == UssdMode.interactiveSingle) {
if (!_isAccessibilityEnabled) {
throw Exception("Accessibility Service must be enabled in settings.");
}
_statusMessage = 'Running interactive single request...';
final response = await UssdService.sendUssdRequest(
ussdCode: code,
subscriptionId: _selectedSimSubscriptionId,
);
setState(() {
_statusMessage = 'Success';
_messageLogs.add('Result: $response');
});
} else {
if (!_isAccessibilityEnabled) {
throw Exception("Accessibility Service must be enabled in settings.");
}
_statusMessage = 'Running multi-session automation...';
final optionsString = _optionsController.text.trim();
final options = optionsString.isNotEmpty
? optionsString.split(',').map((e) => e.trim()).toList()
: <String>[];
await UssdService.multisessionUssd(
code: code,
slotIndex: _selectedSimSlotIndex,
options: options,
overlayMessage: _overlayMessageController.text.trim(),
);
setState(() {
_statusMessage = 'Automation finished / Completed';
});
}
} on PlatformException catch (e) {
setState(() {
_statusMessage = 'Failed with platform error';
_messageLogs.add('Error [${e.code}]: ${e.message}');
});
} catch (e) {
setState(() {
_statusMessage = 'Failed with error';
_messageLogs.add('Error: ${e.toString()}');
});
} finally {
setState(() {
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('THL USSD Service'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _checkStatusAndSims,
)
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status Card
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Permissions & Services', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Accessibility Service:'),
Row(
children: [
Icon(
_isAccessibilityEnabled ? Icons.check_circle : Icons.error,
color: _isAccessibilityEnabled ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await UssdService.openAccessibilitySettings();
},
child: const Text('Settings'),
),
],
)
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Overlay Permission:'),
Row(
children: [
Icon(
_isOverlayPermissionGranted ? Icons.check_circle : Icons.warning,
color: _isOverlayPermissionGranted ? Colors.green : Colors.orange,
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
await UssdService.openOverlaySettings();
},
child: const Text('Settings'),
),
],
)
],
)
],
),
),
),
const SizedBox(height: 16),
// SIM selector
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('SIM Card Selection', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)),
const SizedBox(height: 12),
if (_simCards.isEmpty)
const Text('No SIM cards detected or permissions missing. Click refresh at the top.')
else
DropdownButtonFormField<int>(
value: _selectedSimSubscriptionId,
decoration: const InputDecoration(labelText: 'Select SIM'),
items: _simCards.map((sim) {
return DropdownMenuItem<int>(
value: sim['subscriptionId'],
child: Text('[Slot ${sim['slotIndex']}] ${sim['displayName']} (${sim['carrierName']})'),
);
}).toList(),
onChanged: (val) {
if (val != null) {
final selected = _simCards.firstWhere((element) => element['subscriptionId'] == val);
setState(() {
_selectedSimSubscriptionId = val;
_selectedSimSlotIndex = selected['slotIndex'] ?? 0;
});
}
},
)
],
),
),
),
const SizedBox(height: 16),
// Mode Selector
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Execution Mode', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)),
const SizedBox(height: 12),
RadioListTile<UssdMode>(
title: const Text('Silent Request (TelephonyManager)'),
subtitle: const Text('Completely silent. Android 8+. Only standard *...# codes.'),
value: UssdMode.silent,
groupValue: _ussdMode,
onChanged: (val) => setState(() => _ussdMode = val!),
),
RadioListTile<UssdMode>(
title: const Text('Interactive Single Request (Accessibility)'),
subtitle: const Text('Launches Dialer. Works with #101#451#.'),
value: UssdMode.interactiveSingle,
groupValue: _ussdMode,
onChanged: (val) => setState(() => _ussdMode = val!),
),
RadioListTile<UssdMode>(
title: const Text('Interactive Multi-step Request (Accessibility)'),
subtitle: const Text('Sends multiple menu options sequentially.'),
value: UssdMode.interactiveMulti,
groupValue: _ussdMode,
onChanged: (val) => setState(() => _ussdMode = val!),
),
],
),
),
),
const SizedBox(height: 16),
// USSD parameters input
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Parameters', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)),
const SizedBox(height: 12),
TextField(
controller: _codeController,
decoration: const InputDecoration(
labelText: 'USSD Code',
hintText: 'e.g. *123# or #101#451#',
border: OutlineInputBorder(),
),
),
if (_ussdMode == UssdMode.interactiveMulti) ...[
const SizedBox(height: 12),
TextField(
controller: _optionsController,
decoration: const InputDecoration(
labelText: 'Menu Options (comma separated)',
hintText: 'e.g. 1, 2, 3',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _overlayMessageController,
decoration: const InputDecoration(
labelText: 'Overlay Message',
border: OutlineInputBorder(),
),
),
]
],
),
),
),
const SizedBox(height: 24),
// Execution Button
ElevatedButton(
onPressed: _loading ? null : _executeUssd,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: const Color(0xFF1976D2),
foregroundColor: Colors.white,
),
child: _loading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
)
: const Text('Execute USSD', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 24),
// Outputs and Logs
Card(
color: Colors.black45,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Status Logs', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
Text(_statusMessage, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
],
),
const Divider(height: 24, color: Colors.grey),
if (_messageLogs.isEmpty)
const Text('No activity yet. Run a code to see responses here.', style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey))
else
..._messageLogs.map((log) => Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(log, style: const TextStyle(fontFamily: 'monospace')),
)),
],
),
),
)
],
),
),
);
}
}