ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android电话拨号器开发:Intent与运行时权限实战指南

Android电话拨号器开发:Intent与运行时权限实战指南 简介面向Android开发者的电话拨号器源码包提供了完整的电话应用实现主要适用于移动开发或系统定制的中高级工程师可用于分析拨号器架构、自定义拨号键盘或优化通话流程。资源共56个文件以3个Java源码、20个XML布局/配置、11个class编译文件为核心搭配PNG图片资源、android-support-v4.jar依赖库、proguard混淆配置及Eclipse工程文件压缩后仅875KB目录结构包含src、res、libs、assets等标准模块便于直接导入分析。源码具体覆盖拨号键盘UI设计、DialerActivity拨号逻辑按键监听、号码识别、调用系统API发起呼叫、联系人通过ContentProvider与Cursor同步管理、权限动态申请、基于InCallService的来电去电状态交互并给出异步任务处理耗时操作、内存管理优化、触摸动画与VoIP弱网拨号等场景下的示例代码。目前已有324人学习讨论对于希望深入Android系统服务交互、性能调优或开发特色拨号应用的开发者是一份高价值的参考材料。1. 电话拨号器就是 Android 四大组件教学的最佳切片拿到一个「Android的电话拨号器源码.zip」九成是从课设、毕设或培训班存货里翻出来的老工程一个MainActivity、一块EditText、两个Button点一下就Intent.ACTION_CALL再配一个CALL_PHONE权限。这套东西在 Android 4.x 时代能直接跑放到 Android 14 上大概率一进页面就崩或者点了没反应。原因很简单拨号器虽然是个小项目但它把 Intent 显示跳转、危险权限运行时申请、版本兼容、厂商 ROM 差异全撞上了。这篇不聊这个 zip 里有什么——因为各地的 zip 内容差异很大有的还带lib目录有的连res/layout都缺——而是把你自己重写一个能用的拨号器所需要的完整路径讲清楚原理、最小工程、号码处理、厂商适配和验证收尾。适合正被课设 deadline 追着的人也适合想搞明白ACTION_DIAL与ACTION_CALL边界的中级开发。2. 电话拨号器的地基两个 Intent 的分工与权限边界2.1 为什么会有ACTION_DIAL和ACTION_CALL两个入口电话拨号器的核心不是 UI而是用 Intent 把拨号这件事交给系统。Android 提供了两条路Intent.ACTION_DIAL和Intent.ACTION_CALL。两者的差异并不是能不能拨通而是谁说了算。ACTION_DIAL只做一件事把号码交给系统默认的拨号界面让用户决定要不要拨出去。它不需要任何权限因为你的应用根本没有发起电话只是打开了一个带号码的拨号面板。ACTION_CALL则是直接发起呼叫跳过确认界面因此必须持有android.permission.CALL_PHONE权限且通过运行时检查。// 方案 A打开系统拨号盘用户手动点击拨号键 Uri telUri Uri.parse(tel: number); Intent dialIntent new Intent(Intent.ACTION_DIAL, telUri); startActivity(dialIntent);// 方案 B直接拨号需要 CALL_PHONE 权限 Uri telUri Uri.parse(tel: number); Intent callIntent new Intent(Intent.ACTION_CALL, telUri); startActivity(callIntent);两个方案在代码上只有一个单词之差但行为差异明显。方案 A 适合保存联系人后回拨、历史记录回听这类低风险场景方案 B 适合紧急呼叫、车载模式这类需要一步到位的场景。我一般会在项目里同时保留两个按钮一个叫拨打一个叫预览既演示了权限边界也避免被安全审查卡住。2.2CALL_PHONE权限在 Android 6.0 之后的申请姿势很多老源码里只有uses-permission android:nameandroid.permission.CALL_PHONE /然后在AndroidManifest.xml里完事。这在 targetSdk 22 及以下没问题一旦 targetSdk 升到 23 以上CALL_PHONE属于危险权限组PHONE必须在运行时动态申请。申请流程的标准套路是先检查ContextCompat.checkSelfPermission未授权就调ActivityCompat.requestPermissions然后在onRequestPermissionsResult回调里看用户的决定。注意不要在这里直接开始拨号——用户点击授权按钮后回调时机是确定的但在回调里做 UI 操作时要判断Activity.isFinishing()否则用户快速退出页面会闪退。if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, REQ_CALL_PHONE); } else { doCall(number); // 已授权直接拨号 }REQ_CALL_PHONE是自己定义的请求码只要不与页面里其他请求码冲突即可。注意拒绝过一次和勾选了不再询问是两种状态标准的做法是在申请前用shouldShowRequestPermissionRationale判断——这个方法在用户拒绝过一次之后会返回 true可以借机弹一个说明对话框解释为什么要打电话权限而不是直接再次弹系统弹窗。2.3 拨号器在不同 Android 版本上的行为差异这部分直接决定你的项目能活多久。Android 10API 29之前ACTION_CALL在绝大多数设备上都能直接拉起通话界面。Android 10 开始后台应用发起的ACTION_CALL会被系统拦截除非你的应用是默认拨号器或者处于前台。Android 11API 30引入了包可见性限制queryIntentActivities去探测哪个应用能处理拨号会返回空列表——这个问题经常出现在老代码里表现在点了拨号没反应但不报错。版本关键变化对拨号器的影响Android 6.0运行时权限机制必须动态申请CALL_PHONEAndroid 10后台拨号限制从后台启动ACTION_CALL会被忽略Android 11包可见性queryIntentActivities需要queries声明常见做法是能用ACTION_DIAL就绝不用ACTION_CALL。不要觉得直接拨号更高级在 Android 10 以上ACTION_DIAL依然是零权限、零限制而ACTION_CALL一旦被用户拒绝过权限体验反而更差。3. 用 Android Studio 从零搭一个可用的电话拨号器3.1 项目骨架与布局文件如果手头的 zip 里是一个 Eclipse 工程包结构是src/加gen/没有 Gradle 脚本别想着兼容它直接把MainActivity.java和res/layout里的 XML 拷贝到新工程里重新组织就好。新版 Android Studio 里建一个Empty Views Activity空模板自带activity_main.xml与MainActivity.java语言选 Java 最接近老源码改造阻力最小。布局文件里要有三样东西一个EditText接收号码一个拨号按钮触发ACTION_CALL一个预览按钮触发ACTION_DIAL。不要重复造轮子去做数字键盘——那是自寻烦恼EditText设置android:inputTypephone就能拿到拨号键盘。EditText android:idid/et_number android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint请输入电话号码 android:inputTypephone / Button android:idid/btn_call android:layout_widthwrap_content android:layout_heightwrap_content android:text直接拨打 / Button android:idid/btn_dial android:layout_widthwrap_content android:layout_heightwrap_content android:text拨号面板 /android:inputTypephone很关键它会将软键盘切换为电话键盘且EditText内部自动过滤部分非法字符——注意只是部分#、*、仍然合法这是后续号码处理要面对的坑。布局里的layout_weight让输入框占满剩余宽度按钮固定在右侧这是拨号器最常见的布局形态。3.2 拨号逻辑与权限请求的完整实现在MainActivity.java里两个按钮的点击事件都指向同一个号码输入但走不同的 Intent。直接拨号按钮要经过权限检查权限通过后才真正发起ACTION_CALL。Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); EditText etNumber findViewById(R.id.et_number); findViewById(R.id.btn_call).setOnClickListener(v - { String number etNumber.getText().toString().trim(); if (number.length() 0) { Toast.makeText(this, 号码不能为空, Toast.LENGTH_SHORT).show(); return; } if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, REQ_CALL); } else { call(number); } }); findViewById(R.id.btn_dial).setOnClickListener(v - { String number etNumber.getText().toString().trim(); if (number.length() 0) { Toast.makeText(this, 号码不能为空, Toast.LENGTH_SHORT).show(); return; } dial(number); // ACTION_DIAL 不需要权限 }); } private void call(String number) { Intent intent new Intent(Intent.ACTION_CALL, Uri.parse(tel: number)); startActivity(intent); } private void dial(String number) { Intent intent new Intent(Intent.ACTION_DIAL, Uri.parse(tel: number)); startActivity(intent); }这段代码把权限未授予和已授予两条路径分开了。注意trim()是必要的用户在EditText里输入的号码可能存在末尾空格或换行直接拼进tel:URI 里会导致无法解析。Uri.parse(tel: number)不会对非法字符做转义所以#和*会原样进结果——这在拨打运营商服务号比如*21*时是对的但如果你拿到的是用户粘贴的一段话里带着转1这样的文字就会出问题。3.3 清单文件里必须检查的三个点AndroidManifest.xml在老源码里经常出错尤其是从 Eclipse 时代迁移过来的项目。第一uses-permission必须放在application标签之外放在里面会被合并工具忽略第二CALL_PHONE的权限声明和READ_PHONE_STATE不要搞混很多源码里抄了一堆用不上的权限第三Activity 声明里要有exportedtrue且包含MAIN/LAUNCHER意图过滤器否则应用装上后图标都看不到。uses-permission android:nameandroid.permission.CALL_PHONE / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /applicationexportedtrue是 Android 12API 31之后强制要求的不写会在安装时直接报INSTALL_PARSE_FAILED_MANIFEST_MALFORMED。从老工程里拷贝的 Manifest 可能没有这个属性编译报错时优先检查这里。4. 把号码处理做成拨号器里最有信息量的部分4.1 号码里的特殊字符#、*、与Uri.encode拨号器与普通表单输入最大的不同是号码不是纯数字。#在拨号 URI 里是分机或操作符的语义*表示通配表示国际区号。直接拼接tel:URI 时#在某些实现下会被当作 URI fragment 的起始符——这部分内容不会传给拨号应用导致号码被截断。常见的做法是先用Uri.encode对号码编码再拼进tel:schema。要注意Uri.encode会把/变成%2F但对#、*、的处理各厂商不一致甚至有PhoneNumberUtils这样的专门工具类可以调用。String encodedNumber Uri.encode(rawNumber, #*); Uri uri Uri.parse(tel: encodedNumber); Intent intent new Intent(Intent.ACTION_CALL, uri);这段代码里的第二个参数#*是白名单告诉Uri.encode这些字符不要转义。否则#会被编码成%23部分老版本系统拨号器不识别的%23会把#当作普通字符处理导致运营商服务码拨不出去。这个细节在测试中很容易被忽略——因为手动输入10086没问题但一输入2112345#就静默失败。4.2 双卡手机拨号官方 API 的边界与厂商私有方案多卡拨号是很多真机测试时才暴露的问题。Android 官方从 4.0 开始就没有提供指定 SIM 卡拨号的公开 APIACTION_CALL只能弹出一个系统选择框或者永远用默认卡拨出。SubscriptionManager能查到两张卡的信息但setDefaultDataSubId这类方法在大多数 ROM 上被隐藏或需要系统权限普通应用根本调用不到。方案可行性风险ACTION_CALL 默认卡完全兼容用户无法选卡弹窗引导用户切换默认卡部分 ROM 可用步骤繁琐用户体验差反射调用ITelephony.call需系统签名线上应用绝对不要用华为/小米等厂商私有 API品牌限定代码碎片化严重所以常见做法是做一个选卡提示的AlertDialog告知用户请将默认拨号卡切换为卡1然后继续走ACTION_CALL。不做任何反射或隐藏 API 调用——那是 AOSP 源码里才该做的事普通应用写了就是给自己埋雷。4.3 用通话记录回拨从 ContentResolver 到CallLog.Calls拨号器源码里如果带了最近通话列表那重点不在 UI而在CallLog.Calls的查询。这个系统表可以读出通话类型来电/去电/未接、时间、号码和联系人姓名不需要额外权限但要注意 Android 10 之后查询呼入记录可能需要READ_CALL_LOG权限——这个权限也属于危险权限组需要按运行时权限流程申请。String[] projection { CallLog.Calls._ID, CallLog.Calls.NUMBER, CallLog.Calls.CACHED_NAME, CallLog.Calls.TYPE, CallLog.Calls.DATE }; Cursor cursor getContentResolver().query( CallLog.Calls.CONTENT_URI, projection, null, null, CallLog.Calls.DEFAULT_SORT_ORDER );CallLog.Calls.DEFAULT_SORT_ORDER默认按时间倒序排所以第一个游标记录就是最近一条通话。这里有个参数容易写错CallLog.Calls._ID与CallLog.Calls.NUMBER在部分厂商 ROM 上会返回空原因是系统拨号器没有授予当前应用读取权限——遇到这种情况直接把NUMBER换成CallLog.Calls.PHONE_NUMBER部分源码版本存在再试。5. 用 adb 把拨号流程做成自动化验证再顺手加一个回拨快捷入口很多项目做到能拨号就停了但验收的时候老师或测试会用一台没连接 Android Studio 的手机装 APK点两下说没反应。这时候最有效的验证手段反而不是 UI 点击而是 adb 命令直接拉起拨号链路把每一步的意图和数据打出来。启动应用装好后先验证权限状态adb shell dumpsys package com.example.simple_dialer | grep -E CALL_PHONE|granted输出里如果看到CALL_PHONE: grantedfalse说明权限没授权——直接跑拨号命令会被系统拒绝。接着用am start验证ACTION_CALL与ACTION_DIAL的恢复路径# 验证直接拨号会弹系统权限框或因为未授权而失败 adb shell am start -a android.intent.action.CALL -d tel:10086 # 验证无权限路径打开系统拨号盘 adb shell am start -a android.intent.action.DIAL -d tel:10086如果ACTION_CALL启动后马上回退到桌面大概率是权限问题或 Android 10 以上的后台限制如果ACTION_DIAL也失败检查 Manifest 里的exported属性和queries声明。在真机上跑这几条命令比反复点击 UI 更高效因为它能定位到是权限阻塞还是 Intent 没被解析。验证收尾之后可以给拨号器加一个小入口一个最近一次号码的SharedPreferences存储 桌面长按快捷方式免去每次打开应用输号码的步骤。这个改造只需要 30 行代码却能把用户体验从工具应用变成日常应用。核心逻辑是拨号成功后把号码写进偏好设置桌面快捷方式读取并直接发起ACTION_DIAL。注意这里不要用ACTION_CALL原因是快捷方式从桌面启动属于后台拉起拨号Android 10 以上会被限制而ACTION_DIAL永远安全。这样一个完整的拨号器才闭环——不仅会拨号还知道什么时候该用手上最稳妥的入口。本文还有配套的精品资源点击获取
返回列表