Android 11 旧版本兼容修改:TelephonyManager的getDeviceId和getSubscriberId; WifiManager.setWifiEnabled

随着Android版本的升级,对权限的控制日益严格。例如,TelephonyManager.getSubscriberId()在Android11中因权限问题导致无法使用。同时,setWifiEnabled()在SDK大于等于Q时会返回false,除非具有特定权限。这些变化要求开发者需要考虑更高的targetSdkVersion并遵守更严格的权限管理规定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

    随着Android的更新,越新的版本收紧的权限越来越多,伴随着很多曾经可用的接口慢慢地出现了问题。

比如: TelephonyManager.getSubscriberId()

public static void testGetSubId(Context ctx){
        //Android 11:
        // java.lang.SecurityException: getSubscriberId: The user 10125 does not meet the requirements to access device identifiers.
        TelephonyManager telMgr = (TelephonyManager) ctx.getSystemService(TELEPHONY_SERVICE);
        @SuppressLint("MissingPermission")
        String subId = telMgr.getSubscriberId();
        android.util.Log.d(TAG, "testGetSubId subId=" + subId);
    }

原因

在代码注释中已经写得很明白:
The user 10125 does not meet the requirements to access device identifiers

要做的事情很简单, 找到权限检查的地方, 去掉它
权限检查的位置

frameworks/base/telephony/common/com/android/internal/telephony/TelephonyPermissions.java

    public static boolean checkCallingOrSelfReadDeviceIdentifiers(Context context, int subId,
            String callingPackage, @Nullable String callingFeatureId, String message) {
        return true || checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
                context, subId, callingPackage, callingFeatureId, message, true);
    }
    public static boolean checkCallingOrSelfReadSubscriberIdentifiers(Context context, int subId,
            String callingPackage, @Nullable String callingFeatureId, String message) {
        return true || checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
                context, subId, callingPackage, callingFeatureId, message, false);
    }
    private static boolean checkPrivilegedReadPermissionOrCarrierPrivilegePermission(
            Context context, int subId, String callingPackage, @Nullable String callingFeatureId,
            String message, boolean allowCarrierPrivilegeOnAnySub) {
        int uid = Binder.getCallingUid();
        int pid = Binder.getCallingPid();

        // If the calling package has carrier privileges for specified sub, then allow access.
        if (checkCarrierPrivilegeForSubId(context, subId)) return true;

        // If the calling package has carrier privileges for any subscription
        // and allowCarrierPrivilegeOnAnySub is set true, then allow access.
        if (allowCarrierPrivilegeOnAnySub && checkCarrierPrivilegeForAnySubId(context, uid)) {
            return true;
        }

        PermissionManager permissionManager = (PermissionManager) context.getSystemService(
                Context.PERMISSION_SERVICE);
        if (permissionManager.checkDeviceIdentifierAccess(callingPackage, message, callingFeatureId,
                pid, uid) == PackageManager.PERMISSION_GRANTED) {
            return true;
        }

        return reportAccessDeniedToReadIdentifiers(context, subId, pid, uid, callingPackage,
                message);
    }
    /**
     * Reports a failure when the app with the given pid/uid cannot access the requested identifier.
     *
     * @returns false if the caller is targeting pre-Q and does have the READ_PHONE_STATE
     * permission or carrier privileges.
     * @throws SecurityException if the caller does not meet any of the requirements for the
     *                           requested identifier and is targeting Q or is targeting pre-Q
     *                           and does not have the READ_PHONE_STATE permission or carrier
     *                           privileges.
     */
    private static boolean reportAccessDeniedToReadIdentifiers(Context context, int subId, int pid,
            int uid, String callingPackage, String message) {
        ApplicationInfo callingPackageInfo = null;
        try {
            callingPackageInfo = context.getPackageManager().getApplicationInfoAsUser(
                    callingPackage, 0, UserHandle.getUserHandleForUid(uid));
        } catch (PackageManager.NameNotFoundException e) {
            // If the application info for the calling package could not be found then assume the
            // calling app is a non-preinstalled app to detect any issues with the check
            Log.e(LOG_TAG, "Exception caught obtaining package info for package " + callingPackage,
                    e);
        }
        // The current package should only be reported in StatsLog if it has not previously been
        // reported for the currently invoked device identifier method.
        boolean packageReported = sReportedDeviceIDPackages.containsKey(callingPackage);
        if (!packageReported || !sReportedDeviceIDPackages.get(callingPackage).contains(
                message)) {
            Set invokedMethods;
            if (!packageReported) {
                invokedMethods = new HashSet<String>();
                sReportedDeviceIDPackages.put(callingPackage, invokedMethods);
            } else {
                invokedMethods = sReportedDeviceIDPackages.get(callingPackage);
            }
            invokedMethods.add(message);
            TelephonyCommonStatsLog.write(TelephonyCommonStatsLog.DEVICE_IDENTIFIER_ACCESS_DENIED,
                    callingPackage, message, /* isPreinstalled= */ false, false);
        }
        Log.w(LOG_TAG, "reportAccessDeniedToReadIdentifiers:" + callingPackage + ":" + message + ":"
                + subId);
        // if the target SDK is pre-Q then check if the calling package would have previously
        // had access to device identifiers.
        if (callingPackageInfo != null && (
                callingPackageInfo.targetSdkVersion < Build.VERSION_CODES.Q)) {
            if (context.checkPermission(
                    android.Manifest.permission.READ_PHONE_STATE,
                    pid,
                    uid) == PackageManager.PERMISSION_GRANTED) {
                return false;
            }
            if (checkCarrierPrivilegeForSubId(context, subId)) {
                return false;
            }
        }
        throw new SecurityException(message + ": The user " + uid
                + " does not meet the requirements to access device identifiers.");
    }

frameworks/base/services/core/java/com/android/server/pm/permission/PermissionManagerService.java

    @Override
    public int checkDeviceIdentifierAccess(@Nullable String packageName, @Nullable String message,
            @Nullable String callingFeatureId, int pid, int uid) {
        // If the check is being requested by an app then only allow the app to query its own
        // access status.
        int callingUid = mInjector.getCallingUid();
        int callingPid = mInjector.getCallingPid();
        if (UserHandle.getAppId(callingUid) >= Process.FIRST_APPLICATION_UID && (callingUid != uid
                || callingPid != pid)) {
            String response = String.format(
                    "Calling uid %d, pid %d cannot check device identifier access for package %s "
                            + "(uid=%d, pid=%d)",
                    callingUid, callingPid, packageName, uid, pid);
            Log.w(TAG, response);
            throw new SecurityException(response);
        }
        // Allow system and root access to the device identifiers.
        final int appId = UserHandle.getAppId(uid);
        if (appId == Process.SYSTEM_UID || appId == Process.ROOT_UID) {
            return PackageManager.PERMISSION_GRANTED;
        }
        // Allow access to packages that have the READ_PRIVILEGED_PHONE_STATE permission.
        if (mInjector.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, pid,
                uid) == PackageManager.PERMISSION_GRANTED) {
            return PackageManager.PERMISSION_GRANTED;
        }
        // If the calling package is not null then perform the appop and device / profile owner
        // check.
        if (packageName != null) {
            // Allow access to a package that has been granted the READ_DEVICE_IDENTIFIERS appop.
            long token = mInjector.clearCallingIdentity();
            AppOpsManager appOpsManager = (AppOpsManager) mInjector.getSystemService(
                    Context.APP_OPS_SERVICE);
            try {
                if (appOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, uid,
                        packageName, callingFeatureId, message) == AppOpsManager.MODE_ALLOWED) {
                    return PackageManager.PERMISSION_GRANTED;
                }
            } finally {
                mInjector.restoreCallingIdentity(token);
            }
            // Check if the calling packages meets the device / profile owner requirements for
            // identifier access.
            DevicePolicyManager devicePolicyManager =
                    (DevicePolicyManager) mInjector.getSystemService(Context.DEVICE_POLICY_SERVICE);
            if (devicePolicyManager != null && devicePolicyManager.hasDeviceIdentifierAccess(
                    packageName, pid, uid)) {
                return PackageManager.PERMISSION_GRANTED;
            }
        }
        return PackageManager.PERMISSION_DENIED;
    }


WifiManager.setWifiEnabled

    从官方文档中已经说明: SDK >= Q 这个接口会直接返回 false,不再生效
在这里插入图片描述
示例

build.gradle 注意targetSdkVersion

    defaultConfig {
        applicationId "com.xxx.apitester"
        minSdkVersion 21
        targetSdkVersion 30
		//...
    }
		WifiManager wifiMgr = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        boolean en = wifiMgr.isWifiEnabled();
        //返回false
        boolean res = wifiMgr.setWifiEnabled(!en);

如果调用了从LOG中可以找到: setWifiEnabled not allowed for uid=xxx

原因

frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java

    /**
     * see {@link android.net.wifi.WifiManager#setWifiEnabled(boolean)}
     * @param enable {@code true} to enable, {@code false} to disable.
     * @return {@code true} if the enable/disable operation was
     *         started or is already in the queue.
     */
    @Override
    public synchronized boolean setWifiEnabled(String packageName, boolean enable) {
        if (enforceChangePermission(packageName) != MODE_ALLOWED) {
            return false;
        }
/*********************************/
//检查APP的SDK版本等信息。
/*********************************/
        boolean isPrivileged = isPrivileged(Binder.getCallingPid(), Binder.getCallingUid());
        if (!isPrivileged && !isDeviceOrProfileOwner(Binder.getCallingUid(), packageName)
                && !mWifiPermissionsUtil.isTargetSdkLessThan(packageName, Build.VERSION_CODES.Q,
                  Binder.getCallingUid())
                && !isSystem(packageName, Binder.getCallingUid())) {
            mLog.info("setWifiEnabled not allowed for uid=%")
                    .c(Binder.getCallingUid()).flush();
            return false;
        }
        // If Airplane mode is enabled, only privileged apps are allowed to toggle Wifi
        if (mSettingsStore.isAirplaneModeOn() && !isPrivileged) {
            mLog.err("setWifiEnabled in Airplane mode: only Settings can toggle wifi").flush();
            return false;
        }

        // If SoftAp is enabled, only privileged apps are allowed to toggle wifi
        if (!isPrivileged && mTetheredSoftApTracker.getState() == WIFI_AP_STATE_ENABLED) {
            mLog.err("setWifiEnabled with SoftAp enabled: only Settings can toggle wifi").flush();
            return false;
        }

        mLog.info("setWifiEnabled package=% uid=% enable=%").c(packageName)
                .c(Binder.getCallingUid()).c(enable).flush();
        long ident = Binder.clearCallingIdentity();
        try {
            if (!mSettingsStore.handleWifiToggled(enable)) {
                // Nothing to do if wifi cannot be toggled
                return true;
            }
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        if (mWifiPermissionsUtil.checkNetworkSettingsPermission(Binder.getCallingUid())) {
            if (enable) {
                mWifiMetrics.logUserActionEvent(UserActionEvent.EVENT_TOGGLE_WIFI_ON);
            } else {
                WifiInfo wifiInfo = mClientModeImpl.syncRequestConnectionInfo();
                mWifiMetrics.logUserActionEvent(UserActionEvent.EVENT_TOGGLE_WIFI_OFF,
                        wifiInfo == null ? -1 : wifiInfo.getNetworkId());
            }
        }
        mWifiMetrics.incrementNumWifiToggles(isPrivileged, enable);
        mActiveModeWarden.wifiToggled();
        return true;
    }

检查版本低于:Build.VERSION_CODES.Q

frameworks/opt/net/wifi/service/java/com/android/server/wifi/util/WifiPermissionsUtil.java


    /**
     * Checks whether than the target SDK of the package is less than the specified version code.
     */
    public boolean isTargetSdkLessThan(String packageName, int versionCode, int callingUid) {
    	//强制返回,版本检测将不再生效
    	if(true)return true;
        long ident = Binder.clearCallingIdentity();
        try {
            if (mContext.getPackageManager().getApplicationInfoAsUser(
                    packageName, 0,
                    UserHandle.getUserHandleForUid(callingUid)).targetSdkVersion
                    < versionCode) {
                return true;
            }
        } catch (PackageManager.NameNotFoundException e) {
            // In case of exception, assume unknown app (more strict checking)
            // Note: This case will never happen since checkPackage is
            // called to verify validity before checking App's version.
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
        return false;
    }

参考

  • 第三方APP无法申请READ_PRIVILEGED_PHONE_STATE权限:
Android 10 changes the permissions for device identifiers so that all device identifiers are now protected by the READ_PRIVILEGED_PHONE_STATE permission. 
Prior to Android 10, persistent device identifiers (IMEI/MEID, IMSI, SIM, and build serial) were protected behind the READ_PHONE_STATE runtime permission. 
The READ_PRIVILEGED_PHONE_STATE permission is only granted to apps signed with the platform key and privileged system apps.

How to request for permission: READ_PRIVILEGED_PHONE_STATE?

Android 获取双卡手机IMEI,IMSI,ICCID

<think>我们遇到了一个`java.lang.NoSuchMethodException`,具体是找不到`android.telephony.TelephonyManager.getITelephony`方法。这个错误通常发生在尝试通过反射调用一个在特定Android版本中不存在的方法。下面我将详细解释原因并提供解决方案。 ### 问题原因分析 1. **方法不存在**:`getITelephony()`是`TelephonyManager`的内部方法,并且是`@hide`标注的(即不公开)。不同Android版本中,这个方法可能被移除、改名或访问权限改变。 2. **版本兼容性问题**:在较新的Android版本(如Android 10及以上)中,Google可能移除了这个方法或限制了其访问。 3. **设备厂商定制**:某些设备厂商可能会修改这个方法的实现或完全移除它。 ### 解决方案 #### 方法1:使用替代方法(推荐) 由于`getITelephony()`方法不稳定,我们可以使用其他公开API来实现类似功能。例如,使用`Intent.ACTION_CALL`来拨打电话(需要`CALL_PHONE`权限): ```java Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:" + phoneNumber)); startActivity(intent); ``` #### 方法2:分版本处理反射 如果必须使用反射,我们可以针对不同Android版本使用不同的方法名或类。但注意,这种方法仍然脆弱。 ```java try { TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); Class<?> c = Class.forName(tm.getClass().getName()); Method m; // 根据版本选择方法名 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { m = c.getDeclaredMethod("getITelephony"); } else { // 高版本可能方法名不同,但实际高版本中可能已经移除 m = c.getDeclaredMethod("getITelephony", (Class[]) null); } m.setAccessible(true); Object telephonyService = m.invoke(tm); // 然后调用telephonyService的方法 } catch (Exception e) { e.printStackTrace(); } ``` #### 方法3:使用`TelecomManager`(Android 5.1+) 在Android 5.1 (API 22) 及以上,可以使用`TelecomManager`的`endCall()`方法(需要权限`MODIFY_PHONE_STATE`,但该权限只对系统应用开放): ```java TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); if (tm != null) { tm.endCall(); } ``` > **注意**:`MODIFY_PHONE_STATE`权限在非系统应用中无法使用。 ### 错误处理建议 1. **捕获异常**:在使用反射时,一定要捕获`NoSuchMethodException`、`IllegalAccessException`等异常。 2. **版本检查**:在使用反射前检查Android版本,避免在不支持的版本上调用。 3. **备选方案**:当反射失败时,提供备选方案(如使用Intent拨号)。 ### 示例代码(兼容性处理) ```java public static void endCall(Context context) { try { // 尝试使用Intent结束通话(需要系统权限,通常不可行) // 所以这里我们主要考虑反射 TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); Class<?> c = Class.forName(tm.getClass().getName()); Method m = c.getDeclaredMethod("getITelephony"); m.setAccessible(true); Object telephonyService = m.invoke(tm); Method endCall = telephonyService.getClass().getDeclaredMethod("endCall"); endCall.setAccessible(true); endCall.invoke(telephonyService); } catch (NoSuchMethodException e) { // 方法不存在,尝试其他方案 Log.e("Reflection", "Method not found", e); // 使用Intent打开拨号界面(无法直接挂断) Intent intent = new Intent(Intent.ACTION_DIAL); context.startActivity(intent); } catch (Exception e) { Log.e("Reflection", "Error ending call", e); } } ``` ### 兼容性说明 | Android 版本 | 可用性 | |--------------|----------------------| | < 5.0 | `getITelephony` 存在但需要反射 | | 5.0 - 6.0 | 方法可能变动 | | > 7.0 | 可能完全移除 | ### 注意事项 1. **权限声明**:即使使用反射,也需要在`AndroidManifest.xml`中添加权限(但非公开API的权限可能无效): ```xml <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> ``` 注意,这个权限在非系统应用中无法获得。 2. **系统应用**:如果应用是系统应用(预装在`/system/priv-app`),则可以申请`MODIFY_PHONE_STATE`权限。 3. **厂商适配**:对于特定设备(如华为、小米),可能需要额外的适配,因为厂商可能修改了AOSP的实现。 ### 替代方案总结 - 对于挂断电话:没有完美的解决方案,除非是系统应用。 - 对于拨打电话:使用`Intent.ACTION_CALL`或`Intent.ACTION_DIAL`。 ### 相关问题 1. 如何在没有用户交互的情况下挂断电话? 2. Android系统应用开发有哪些特殊权限? 3. 如何适配不同厂商的Android系统? > **重要提示**:由于Android版本碎片化厂商定制,使用隐藏API的风险很高,建议尽量避免使用反射调用系统内部方法[^1]。 [^1]: 反射调用隐藏API可能导致应用在系统更新后崩溃</think>### `NoSuchMethodException: getITelephony` 错误分析与解决方案 #### 问题原因 `java.lang.NoSuchMethodException` 表示反射调用时找不到目标方法。具体到 `android.telephony.TelephonyManager.getITelephony` 方法: 1. **隐藏 API 限制**:`getITelephony()` 是 Android 内部隐藏 API(`@hide` 标注),非公开接口 2. **版本兼容性问题**:不同 Android 版本中: - Android 4.x 存在此方法 - Android 5.0+ 方法签名变更或移除 - Android 10+ 完全移除此方法[^1] 3. **厂商定制差异**:华为、小米等厂商可能修改/移除该方法[^3] --- ### 解决方案(分版本处理) #### 1. 通用反射调用模板(兼容 Android 4.x - 9.0) ```java public static Object getITelephony(TelephonyManager tm) throws Exception { Class<?> telephonyManagerClass = Class.forName(tm.getClass().getName()); Method getITelephonyMethod; // Android 5.0+ 需要新方法签名 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getITelephonyMethod = telephonyManagerClass.getDeclaredMethod( "getITelephony", (Class[]) null ); } else { getITelephonyMethod = telephonyManagerClass.getDeclaredMethod("getITelephony"); } getITelephonyMethod.setAccessible(true); return getITelephonyMethod.invoke(tm); } ``` #### 2. Android 10+ 替代方案(使用公开 API) ```java // 需要权限:<uses-permission android:name="android.permission.CALL_PHONE"/> public static void endCallOnAndroidQPlus(Context context) { TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); if (tm != null) { tm.endCall(); } } ``` #### 3. 厂商特例处理(如华为设备) ```java // 华为设备特殊处理 if (Build.MANUFACTURER.equalsIgnoreCase("huawei")) { try { Class<?> hwTelephonyClass = Class.forName("com.huawei.android.telephony.TelephonyManagerEx"); Method getDefault = hwTelephonyClass.getMethod("getDefault"); Object hwTelephony = getDefault.invoke(null); Method endCall = hwTelephonyClass.getMethod("endCall"); endCall.invoke(hwTelephony); return; } catch (Exception e) { Log.e("HuaweiFallback", "Huawei API failed", e); } } ``` --- ### 完整调用示例(兼容版) ```java public static void endCall(Context context) { try { TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); // 分版本处理 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { endCallOnAndroidQPlus(context); return; } Object telephony = getITelephony(tm); Method endCallMethod = telephony.getClass().getDeclaredMethod("endCall"); endCallMethod.invoke(telephony); } catch (NoSuchMethodException e) { // 处理厂商兼容性问题 handleVendorSpecificEndCall(context); } catch (Exception e) { Log.e("Telephony", "endCall failed", e); } } ``` --- ### 关键注意事项 1. **权限声明**: ```xml <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/> <uses-permission android:name="android.permission.CALL_PHONE"/> ``` > **注意**:`MODIFY_PHONE_STATE` 仅系统应用可用 2. **版本兼容表**: | Android 版本 | 可用方案 | |-------------|----------------------------| | 4.x (API 14-19) | `getITelephony()` 直接反射 | | 5.x-9.0 (API 21-28) | 带参数反射 | | 10.0+ (API 29+) | `TelecomManager.endCall()` | 3. **异常处理建议**: ```java catch (NoSuchMethodException e) { // 1. 尝试备用方法名 tryMethod("getITelephonyEx"); // 2. 记录设备信息便于适配 Log.w("UnsupportedDevice", "Model: " + Build.MODEL + " SDK: " + Build.VERSION.SDK_INT); } ``` --- ### 厂商适配建议 1. **华为设备**: ```java // 使用华为扩展包 dependencies { implementation 'com.huawei.hms:telephony:6.4.0.300' } // 调用:com.huawei.hms.tm.TelephonyManagerEx.endCall() ``` 2. **小米设备**: ```java // 需要申请额外权限 if (Build.MANUFACTURER.equals("Xiaomi")) { ActivityCompat.requestPermissions( activity, new String[]{"android.permission.MANAGE_OWN_CALLS"}, REQ_CODE ); } ``` --- ### 替代方案推荐 1. **使用可访问 API**: ```java // 通过系统界面触发挂断 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENDCALL)); context.sendOrderedBroadcast(intent, null); ``` 2. **无障碍服务方案**: ```java // 在无障碍服务中执行 public class CallService extends AccessibilityService { @Override public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { performGlobalAction(GLOBAL_ACTION_END_CALL); } } } ``` > **法律提示**:非系统应用调用电话控制功能可能违反 Google Play 政策,需明确告知用户功能用途[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值