flutter 获取通话记录和通讯录

复制代码
Dart SDK version is 3.7.0

1 
复制代码
dependencies:
  flutter:
    sdk: flutter
  permission_handler: ^11.0.1  # 权限管理
  flutter_contacts: ^1.1.9+2
  call_log: ^5.0.5
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
复制代码
2  contact_and_calls_page.dart

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:call_log/call_log.dart';

class ContactAndCallsPage extends StatefulWidget {
  @override
  _ContactAndCallsPageState createState() => _ContactAndCallsPageState();
}

class _ContactAndCallsPageState extends State<ContactAndCallsPage>
    with SingleTickerProviderStateMixin {
  List<Contact> _contacts = [];
  Iterable<CallLogEntry> _callLogs = [];
  bool _isLoading = true;
  String _errorMessage = '';
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    _requestPermissions();
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  // 请求权限
  Future<void> _requestPermissions() async {
    setState(() {
      _isLoading = true;
      _errorMessage = '';
    });

    try {
      // Android 特定权限处理
      var contactStatus = await Permission.contacts.request();
      var phoneStatus = await Permission.phone.request();

      // 检查是否获得权限
      if (contactStatus.isGranted || phoneStatus.isGranted) {
        await _fetchContacts();
        await _fetchCallLogs();
      } else {
        setState(() {
          _errorMessage = '需要通讯录和通话记录权限以正常使用功能';
        });
        _showPermissionDeniedDialog();
      }
    } catch (e) {
      setState(() {
        _errorMessage = '权限请求发生错误: $e';
      });
      print('权限请求错误: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  // 显示权限被拒绝的对话框
  void _showPermissionDeniedDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('权限受限'),
        content: Text(_errorMessage),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              openAppSettings(); // 打开应用设置
            },
            child: Text('打开设置'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('取消'),
          ),
        ],
      ),
    );
  }

  // 获取通讯录联系人
  Future<void> _fetchContacts() async {
    try {
      // 多重权限检查
      bool permission = await FlutterContacts.requestPermission(readonly: true);

      if (permission) {
        // 添加详细配置获取联系人
        final contacts = await FlutterContacts.getContacts(
          withProperties: true,   // 获取联系人属性
          withPhoto: false,        // 不获取照片以提高性能
          sorted: true,            // 按名称排序
        );

        // 过滤掉没有电话号码的联系人
        final filteredContacts = contacts.where((contact) =>
        contact.phones.isNotEmpty
        ).toList();

        setState(() {
          _contacts = filteredContacts;
        });

        print('获取到联系人数量: ${_contacts.length}');
      } else {
        setState(() {
          _errorMessage = '未获得通讯录权限';
        });
      }
    } catch (e) {
      print('获取通讯录错误: $e');
      setState(() {
        _errorMessage = '获取通讯录失败: $e';
      });
    }
  }

  // 获取通话记录
  Future<void> _fetchCallLogs() async {
    try {
      // 获取通话记录
      Iterable<CallLogEntry> callLogs = await CallLog.get();

      // 过滤最近30天的通话记录
      final now = DateTime.now();
      final thirtyDaysAgo = now.subtract(Duration(days: 30));

      setState(() {
        _callLogs = callLogs
            .where((log) {
          final logTime = DateTime.fromMillisecondsSinceEpoch(log.timestamp ?? 0);
          return logTime.isAfter(thirtyDaysAgo);
        })
            .take(100)  // 限制最多100条记录
            .toList();
      });

      print('获取到通话记录数量: ${_callLogs.length}');
    } catch (e) {
      print('获取通话记录错误: $e');
      setState(() {
        _errorMessage = '获取通话记录失败: $e';
      });
    }
  }

  // 转换通话类型
  String _getCallType(CallType? callType) {
    switch (callType) {
      case CallType.incoming:
        return '呼入';
      case CallType.outgoing:
        return '呼出';
      case CallType.missed:
        return '未接';
      default:
        return '未知';
    }
  }

  // 格式化通话记录时间
  String _formatCallLogTime(int? timestamp) {
    if (timestamp == null) return '未知时间';
    final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
    return '${dateTime.year}-${_twoDigits(dateTime.month)}-${_twoDigits(dateTime.day)} '
        '${_twoDigits(dateTime.hour)}:${_twoDigits(dateTime.minute)}:${_twoDigits(dateTime.second)}';
  }

  // 补充两位数方法
  String _twoDigits(int n) {
    return n.toString().padLeft(2, '0');
  }

  // 格式化通话时长
  String _formatCallDuration(int? duration) {
    if (duration == null || duration == 0) return '未接通';

    final minutes = duration ~/ 60;
    final seconds = duration % 60;

    return '$minutes分${_twoDigits(seconds)}秒';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('通讯录和通话记录'),
        bottom: TabBar(
          controller: _tabController,
          tabs: [
            Tab(icon: Icon(Icons.contacts), text: '通讯录'),
            Tab(icon: Icon(Icons.call), text: '通话记录'),
          ],
        ),
        actions: [
          IconButton(
            icon: Icon(Icons.refresh),
            onPressed: _requestPermissions, // 直接调用权限请求方法
          ),
        ],
      ),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : _errorMessage.isNotEmpty
          ? Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.error_outline, color: Colors.red, size: 100),
            SizedBox(height: 20),
            Text(
              _errorMessage,
              style: TextStyle(color: Colors.red),
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _requestPermissions,
              child: Text('重新获取权限'),
            )
          ],
        ),
      )
          : TabBarView(
        controller: _tabController,
        children: [
          _buildContactsList(),
          _buildCallLogsList(),
        ],
      ),
    );
  }

  // 构建通讯录列表
  Widget _buildContactsList() {
    if (_contacts.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.contacts, size: 100, color: Colors.grey),
            SizedBox(height: 20),
            Text(
              '暂无联系人',
              style: TextStyle(fontSize: 18, color: Colors.grey),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: _contacts.length,
      itemBuilder: (context, index) {
        Contact contact = _contacts[index];
        return ListTile(
          leading: CircleAvatar(
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              contact.name.first.isNotEmpty ? contact.name.first[0] : '?',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ),
          title: Text(
            contact.name.first.isNotEmpty ? contact.name.first : '未命名',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          subtitle: Text(
            contact.phones.isNotEmpty
                ? contact.phones.first.number
                : '无电话号码',
            style: TextStyle(color: Colors.grey[600]),
          ),
        );
      },
    );
  }

  // 构建通话记录列表
  Widget _buildCallLogsList() {
    if (_callLogs.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.call, size: 100, color: Colors.grey),
            SizedBox(height: 20),
            Text(
              '暂无通话记录',
              style: TextStyle(fontSize: 18, color: Colors.grey),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: _callLogs.length,
      itemBuilder: (context, index) {
        CallLogEntry callLog = _callLogs.elementAt(index);
        return ListTile(
          leading: Icon(
            callLog.callType == CallType.missed
                ? Icons.call_missed
                : (callLog.callType == CallType.incoming
                ? Icons.call_received
                : Icons.call_made),
            color: callLog.callType == CallType.missed
                ? Colors.red
                : Colors.green,
          ),
          title: Text(callLog.number ?? '未知号码'),
          subtitle: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('时间: ${_formatCallLogTime(callLog.timestamp)}'),
              Text('类型: ${_getCallType(callLog.callType)}'),
              Text('通话时长: ${_formatCallDuration(callLog.duration)}'),
            ],
          ),
        );
      },
    );
  }
}  

3 main.dart

复制代码
import 'package:flutter/material.dart';
import 'contact_and_calls_page.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '通讯录和通话记录',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ContactAndCallsPage(),
    );
  }
}

4 降级java sdk到1.8 --- build.gradle.kts

复制代码
plugins {
    id("com.android.application")
    id("kotlin-android")
    id("dev.flutter.flutter-gradle-plugin")
}

android {
    namespace = "com.example.contactlist"
    compileSdk = flutter.compileSdkVersion
    ndkVersion = flutter.ndkVersion

    compileOptions {
        // Java 8 配置
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8

        // 启用 Core Library Desugaring
        isCoreLibraryDesugaringEnabled = true
    }

    kotlinOptions {
        // 匹配 Java 版本
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

    defaultConfig {
        applicationId = "com.example.contactlist"
        minSdk = 21  // 确保 minSdk 不低于 21
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName

        // 启用 MultiDex
        multiDexEnabled = true
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}

dependencies {
    // 升级到 2.1.4 或更高版本
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")

    // MultiDex 支持
    implementation("androidx.multidex:multidex:2.0.1")
}

flutter {
    source = "../.."
}
相关推荐
whoarethenext18 分钟前
qt的基本使用
开发语言·c++·后端·qt
atec200040 分钟前
使用uv管理python项目环境
开发语言·python·uv
是僵尸不是姜丝3 小时前
每日算法:洛谷U535992 J-C 小梦的宝石收集(双指针、二分)
c语言·开发语言·算法
小画家~3 小时前
第二十二: go与k8s、docker相关编写dockerfile
开发语言·golang·kubernetes
anlogic4 小时前
Java基础 4.12
java·开发语言
海涛高软4 小时前
qt mapFrom返回的QPoint和event->pos()区别和globalPos区别
开发语言·qt·命令模式
lauo4 小时前
智体知识库:ai-docs对分布式智体编程语言Poplang和javascript的语法的比较(知识库问答)
开发语言·前端·javascript·分布式·机器人·开源
xiegwei4 小时前
Kotlin 和 spring-cloud-function 兼容问题
开发语言·kotlin·springcloud
Alt.94 小时前
SpringMVC基础二(RestFul、接收数据、视图跳转)
java·开发语言·前端·mvc
念九_ysl5 小时前
深入解析Java内存与缓存:从原理到实践优化
java·开发语言·缓存